{
  "openapi": "3.0.3",
  "info": {
    "title": "SOAT REST API",
    "version": "0.91.0",
    "description": "Complete REST surface of a SOAT deployment, merged from the per-module OpenAPI\nspecs that also generate the TypeScript SDK, the `soat` CLI and the MCP tool\nsurface. SOAT is self-hosted: point the `baseUrl` server variable at your own\ndeployment — this documentation site serves the description only, not the API.\n\nAuthentication is a bearer token: a user JWT from `POST /api/v1/users/login`,\nor a project key (`sk_…`). Request and response bodies are snake_case.\n\nEvery error response — including the ones a client is most tempted to\nspecial-case (401, 403, 429, 500) — is JSON of the same shape:\n`{ \"error\": { \"code\": \"RESOURCE_NOT_FOUND\", \"message\": \"…\", \"hint\": \"…\",\n\"docs_url\": \"…\", \"meta\": { … } } }`.\n`error.code` is stable and safe to branch on, and `error.hint` says what to do\nabout the failure; the full catalog of codes, their\nHTTP statuses and what they mean is in this document's `x-error-codes`\nextension 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": "Deciders",
      "description": "Manage deciders and request decisions from them"
    },
    {
      "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 individual memories (the knowledge items stored in a memory store)"
    },
    {
      "name": "Memory Rules",
      "description": "What a memory store accepts from completed agent turns"
    },
    {
      "name": "MemoryStores",
      "description": "Manage memory store configurations for document retrieval"
    },
    {
      "name": "Metadata Schemas",
      "description": "Declare the structure a resource's metadata must satisfy"
    },
    {
      "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": "Playbooks",
      "description": "Manage playbooks and the cases filled against them"
    },
    {
      "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": "Shares",
      "description": "Share a tool or an agent with another project"
    },
    {
      "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 raw usage events"
    },
    {
      "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, severity, agent, generation and orchestration run — every filter composable with the rest. 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_created",
                "approval_resolved",
                "exception_created",
                "schedule_fired",
                "share_cap_exceeded",
                "share_resumed",
                "share_revoked",
                "share_suspended",
                "tool_resolution_failed",
                "usage_quantity_invalid"
              ]
            }
          },
          {
            "name": "severity",
            "in": "query",
            "description": "Filter by severity",
            "schema": {
              "type": "string",
              "enum": [
                "info",
                "warning",
                "critical"
              ]
            }
          },
          {
            "name": "agent_id",
            "in": "query",
            "description": "Filter to entries produced by one agent. Composable with every other filter — entries carry one agent, one generation and one run, so naming two narrows to the rows where both hold.",
            "schema": {
              "type": "string",
              "example": "agent_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "generation_id",
            "in": "query",
            "description": "Filter to entries produced during one agent generation. This is what turns `tool_resolution_failed` from alertable into usable: the warning for a suspect turn is one query rather than a scan of the project's feed.",
            "schema": {
              "type": "string",
              "example": "gen_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "orchestration_run_id",
            "in": "query",
            "description": "Filter to entries produced during one orchestration run.",
            "schema": {
              "type": "string",
              "example": "orch_run_V1StGXR8Z5jdHi"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque cursor from a previous page's `next_cursor`",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "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/activity/export": {
      "get": {
        "tags": [
          "Activity"
        ],
        "summary": "Export the activity feed as NDJSON",
        "description": "Streams a project's activity entries as newline-delimited JSON — one entry object per line, **oldest first**, where the feed itself reads newest first: a feed answers what just happened, and a file is read start to end. `project_id` is required: the export is per-project by design. Filters behave exactly as they do on the list endpoint.",
        "operationId": "exportActivity",
        "x-soat-mcp-exclude": true,
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "required": true,
            "description": "Project whose activity is exported",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "description": "Only entries of this kind",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "severity",
            "in": "query",
            "description": "Only entries of this severity",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "agent_id",
            "in": "query",
            "description": "Only entries an agent produced",
            "schema": {
              "type": "string",
              "example": "agent_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "generation_id",
            "in": "query",
            "description": "Only entries a generation produced",
            "schema": {
              "type": "string",
              "example": "gen_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "orchestration_run_id",
            "in": "query",
            "description": "Only entries an orchestration run produced",
            "schema": {
              "type": "string",
              "example": "orch_run_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A newline-delimited stream of activity entries. Each line is a JSON object with the same fields as `ActivityEntry`.",
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "`project_id` is required"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      }
    },
    "/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": "name",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring match on the actor name",
            "schema": {
              "type": "string",
              "example": "Ada"
            }
          },
          {
            "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).\n",
            "schema": {
              "type": "string",
              "example": "conv_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/TagsQuery"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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"
                ],
                "additionalProperties": false,
                "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",
        "x-soat-resource": {
          "kind": "actor",
          "from": "actor_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "actor",
          "from": "actor_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "actor",
          "from": "actor_id"
        },
        "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": false,
                "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": {
                    "$ref": "#/components/schemas/TagBag"
                  }
                }
              }
            }
          }
        },
        "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",
        "x-soat-resource": {
          "kind": "actor",
          "from": "actor_id"
        },
        "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": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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",
        "x-soat-resource": {
          "kind": "actor",
          "from": "actor_id"
        },
        "parameters": [
          {
            "name": "actor_id",
            "in": "path",
            "required": true,
            "description": "Actor ID",
            "schema": {
              "type": "string",
              "example": "actor_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tags replaced",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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 into the actor's existing tags (existing tags are preserved unless overridden)",
        "operationId": "mergeActorTags",
        "x-soat-resource": {
          "kind": "actor",
          "from": "actor_id"
        },
        "parameters": [
          {
            "name": "actor_id",
            "in": "path",
            "required": true,
            "description": "Actor ID",
            "schema": {
              "type": "string",
              "example": "actor_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tags merged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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. A credential scoped to a project the agent is shared with, through an accepted share, reads its `id` and `name` only.\n",
        "operationId": "getAgent",
        "x-soat-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          }
        ],
        "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",
        "x-soat-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          },
          {
            "$ref": "#/components/parameters/IfMatchVersion"
          }
        ],
        "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"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      },
      "patch": {
        "tags": [
          "Agents"
        ],
        "summary": "Partially update an agent",
        "description": "Partially updates an existing agent. Identical to PUT — both perform partial updates.",
        "operationId": "patchAgent",
        "x-soat-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          },
          {
            "$ref": "#/components/parameters/IfMatchVersion"
          }
        ],
        "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"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      },
      "delete": {
        "tags": [
          "Agents"
        ],
        "summary": "Delete an agent",
        "description": "Deletes an agent by ID. Fails with `409` if the agent has dependent generations or traces, or another project has accepted a share of it, unless `force=true` is passed, in which case those generations and traces are deleted along with the agent and the shares are revoked. Every share of the agent is revoked when it is deleted.\n",
        "operationId": "deleteAgent",
        "x-soat-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          },
          {
            "name": "force",
            "in": "query",
            "required": false,
            "description": "When `true`, deletes the agent's dependent generations and traces, revokes its accepted shares and leaves its ingestion rules naming it instead of returning `409 AGENT_HAS_DEPENDENTS`.\n",
            "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, traces, accepted shares or ingestion rules in its own project (pass `force=true` to delete anyway; records and rules another project made through a share are kept). `error.meta` carries `generation_count`, `trace_count`, `accepted_share_count` and `ingestion_rule_count` so a caller can tell which one is nonzero.\n",
            "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. Pass `idempotency_key` to make a retry safe: a request whose key is already claimed runs nothing and answers `202` with the generation the key names.\n\nA credential scoped to a project the agent is shared with, through an accepted share, runs it in that project: on the publisher's configuration, recorded, metered and governed in the grantee project.\n",
        "operationId": "createAgentGeneration",
        "x-soat-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "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 tool call, `builtin` or MCP, 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]`.\n\nA turn that pauses on a `client` tool ends with one more frame before `[DONE]`: the generation as the non-streaming path returns it, `status: requires_action` with `required_action` naming the pending calls. Submit their results to `tool-outputs` to resume.\n\nThe 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`.\n"
                }
              }
            }
          },
          "202": {
            "description": "Generation accepted and running in the background (default, when `wait` is omitted or `false`), or — in every mode, streamed included — a duplicate request: the generation the `idempotency_key` already names is returned in whatever state it has reached and nothing runs. Poll `GET /api/v1/generations/{generation_id}` for the result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AcceptedGenerationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request (e.g. an `idempotency_key` that is not a non-empty string of at most 255 characters)",
            "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"
                }
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_KEY_REUSED` — the key is already claimed by a generation started from a different request. Nothing runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`QUOTA_EXCEEDED`: an `enforce`-mode generation quota is exhausted; `error.meta` carries `quota_id`, `metric`, `limit`, `window` and `resets_at`. `SHARE_CAP_EXCEEDED`: the agent is another project's, reached through a share whose `cap` is spent for the window; `error.meta.retry_after` says when. Both carry a `Retry-After` header.",
            "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.\n",
            "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.\n",
        "operationId": "submitAgentToolOutputs",
        "x-soat-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          },
          {
            "$ref": "#/components/parameters/GenerationId"
          }
        ],
        "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"
                }
              }
            }
          },
          "409": {
            "description": "The generation is not paused on client tool calls (GENERATION_NOT_AWAITING_TOOL_OUTPUTS): it never paused, or its outputs were already submitted. Each pause accepts outputs once.\n",
            "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`.\n",
            "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).\n",
        "operationId": "listAgentVersions",
        "x-soat-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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.\n",
        "operationId": "getAgentVersion",
        "x-soat-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          },
          {
            "$ref": "#/components/parameters/AgentVersionNumber"
          }
        ],
        "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.\n\nThe 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.\n",
        "operationId": "restoreAgentVersion",
        "x-soat-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          },
          {
            "$ref": "#/components/parameters/AgentVersionNumber"
          }
        ],
        "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"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      }
    },
    "/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`.\n\nAssignment 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.\n\nWhile 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`.\n",
        "operationId": "setAgentRelease",
        "x-soat-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          }
        ],
        "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.\n\nWhen 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.\n",
        "operationId": "promoteAgentRelease",
        "x-soat-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          }
        ],
        "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.\n",
        "operationId": "abortAgentRelease",
        "x-soat-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          }
        ],
        "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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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.\n\nA `bedrock` or `vertex` record must carry a credential of its own — a\nlinked `secret_id`, or an `apiKey` in `config`. Without one the provider\nSDK signs with the server's own credentials (the AWS default credential\nchain, Google Application Default Credentials), which is refused with\n`400 VALIDATION_FAILED` unless the deployment allows it.\n",
        "operationId": "createAiProvider",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "provider",
                  "default_model"
                ],
                "additionalProperties": false,
                "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. Required for `bedrock` and `vertex` unless `config.apiKey` carries one.",
                    "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",
                "additionalProperties": false,
                "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.\n\nLive 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 its usage history, preserving those rows: each usage event keeps the provider's id as its `ai_provider_id`. 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.\n",
        "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.\n",
            "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.\n"
          }
        }
      }
    },
    "/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.\nWhich 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.\nNot 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`.\nListing 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 — IAM keys or a Bedrock API key, a Google service-account key. A record with no `secret_id` would fall back to the server's own credentials (the AWS default credential chain, Google Application Default Credentials); it can list only on a deployment that allows a record to use them, and returns `400 AI_PROVIDER_MISCONFIGURED` otherwise.\nA 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`.\nThe 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.\nThe provider answers its whole catalogue in one call; `q`, `limit` and `offset` narrow and page what it returned. A request without `limit` gets the first 50 models.\n",
        "operationId": "listAiProviderModels",
        "parameters": [
          {
            "name": "ai_provider_id",
            "in": "path",
            "required": true,
            "description": "AI Provider ID",
            "schema": {
              "type": "string",
              "example": "aip_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Keeps the models whose `id` or `display_name` contains this text, ignoring case. Applied before paging, so `total` counts the matches.\n",
            "schema": {
              "type": "string",
              "example": "sonnet"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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).\n"
          },
          "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.\n",
        "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.\n",
        "operationId": "updateAiProviderPrices",
        "x-soat-agent-exclude": true,
        "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 the caller's own\n  keys scoped to that project, or every key scoped to it when the credential\n  holds `api-keys:ListApiKeys` on the project. Confinement to a project is not\n  authority over it, and a key's metadata names its owner, prefix and attached\n  policies.\n",
        "operationId": "listApiKeys",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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.\n",
        "operationId": "createApiKey",
        "x-soat-agent-exclude": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "additionalProperties": false,
                "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",
        "x-soat-agent-exclude": true,
        "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",
        "x-soat-agent-exclude": true,
        "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",
                "additionalProperties": false,
                "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",
        "x-soat-agent-exclude": true,
        "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",
            "required": false,
            "description": "Filter by lifecycle status. Repeat the parameter to OR values. A value outside the enum, empty string included, is a `400`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "pending",
                  "approved",
                  "rejected",
                  "expired"
                ]
              }
            },
            "style": "form",
            "explode": true
          },
          {
            "name": "origin",
            "in": "query",
            "description": "Filter by producer origin",
            "schema": {
              "type": "string",
              "enum": [
                "node",
                "tool_call",
                "task_transition"
              ]
            }
          },
          {
            "name": "expires_from",
            "in": "query",
            "description": "Only items whose `expires_at` is at or after this timestamp (ISO 8601)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "expires_to",
            "in": "query",
            "description": "Only items whose `expires_at` is at or before this timestamp (ISO 8601)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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, or an `expires_from` / `expires_to` that is not an ISO 8601 timestamp"
          },
          "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",
            "required": false,
            "description": "Lifecycle statuses the groups are built from (default `rejected`). Repeat the parameter to OR values. A value outside the enum, empty string included, is a `400`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "pending",
                  "approved",
                  "rejected",
                  "expired"
                ]
              },
              "default": [
                "rejected"
              ]
            },
            "style": "form",
            "explode": true
          },
          {
            "name": "min_count",
            "in": "query",
            "description": "Minimum items in a group for it to be returned",
            "schema": {
              "type": "integer",
              "default": 2,
              "minimum": 1
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/approval_id"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "arguments": {
                    "type": "object",
                    "description": "Edited arguments to execute instead of the proposed ones. Editing composes a new call, so it additionally requires permission to make that call: `tools:CallTool` on the tool, and for a `builtin` proposal the action's own IAM action."
                  },
                  "tool_context": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Key-value pairs forwarded as context headers on the approved action and on every tool call of the continuation turn, as on a generation's `tool_context`. Never stored on the item: supply it on the approve call itself, for a tool that authorizes with a credential minted at decision time. The server-pinned identity keys (`session_id`, `actor_id`, `actor_external_id`) are dropped. An invalid or colliding key is rejected with `400 INVALID_TOOL_CONTEXT_KEY` and nothing is resolved."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approval item approved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApprovalItem"
                }
              }
            }
          },
          "400": {
            "description": "Edited arguments are not a JSON object, or do not satisfy the tool's parameters schema; or a `tool_context` key is not a valid header name"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — including an edit by a caller who may resolve the item but not make the call it proposes"
          },
          "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",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/approval_id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason"
                ],
                "additionalProperties": false,
                "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": "Results per page. Above 200 it is clamped, and `limit` in the response reports the page served; below 1 or not an integer it is `400 VALIDATION_FAILED`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 25
            }
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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 not a valid ISO 8601 date, or `limit` / `offset` is not a valid page"
          },
          "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",
        "x-soat-resource": {
          "kind": "audit",
          "from": "entry_id"
        },
        "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",
            "required": false,
            "description": "Filter by chain status. Repeat the parameter to OR values. A value outside the enum, empty string included, is a `400`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "active",
                  "concluded",
                  "expired",
                  "budget_exhausted"
                ]
              }
            },
            "style": "form",
            "explode": true
          },
          {
            "name": "agent_id",
            "in": "query",
            "description": "Filter by the agent whose continuation opened the chain",
            "schema": {
              "type": "string",
              "example": "agent_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
        "x-soat-resource": {
          "kind": "chain",
          "from": "chain_id"
        },
        "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"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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.\n\nNames 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.\n\nSystem 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.\n\nMessages may use `document_id` instead of `content` with either target. Chats hold no message history — send the full `messages` array every time.\n",
        "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]`.\nThe 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]`.\n"
                }
              }
            }
          },
          "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.\n",
            "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"
            }
          },
          {
            "$ref": "#/components/parameters/TagsQuery"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
                "additionalProperties": false,
                "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"
                  },
                  "retrieval": {
                    "type": "string",
                    "enum": [
                      "embed",
                      "none",
                      null
                    ],
                    "nullable": true,
                    "description": "Whether this conversation's turns are embedded for vector retrieval. Turns are stored and chunked either way, so `none` leaves them readable and reachable by full-text search without paying for an embedding. `null` (the default) inherits the project's `default_conversation_retrieval`."
                  }
                }
              }
            }
          }
        },
        "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",
        "x-soat-resource": {
          "kind": "conversation",
          "from": "conversation_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "conversation",
          "from": "conversation_id"
        },
        "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": false,
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "open",
                      "closed"
                    ],
                    "description": "New conversation status"
                  },
                  "name": {
                    "type": "string",
                    "nullable": true,
                    "description": "New conversation name"
                  },
                  "retrieval": {
                    "type": "string",
                    "enum": [
                      "embed",
                      "none",
                      null
                    ],
                    "nullable": true,
                    "description": "Whether this conversation's turns are embedded for vector retrieval. Turns are stored and chunked either way, so `none` leaves them readable and reachable by full-text search without paying for an embedding. `null` (the default) inherits the project's `default_conversation_retrieval`. Switching it on embeds the turns already in the conversation, so the whole conversation becomes retrievable rather than only what is said next."
                  }
                }
              }
            }
          }
        },
        "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",
        "x-soat-resource": {
          "kind": "conversation",
          "from": "conversation_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "conversation",
          "from": "conversation_id"
        },
        "parameters": [
          {
            "name": "conversation_id",
            "in": "path",
            "required": true,
            "description": "Conversation ID",
            "schema": {
              "type": "string",
              "example": "conv_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
        "x-soat-resource": {
          "kind": "conversation",
          "from": "conversation_id"
        },
        "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"
                ],
                "additionalProperties": false,
                "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": {
                    "description": "Caller-owned annotations on the message (e.g. a phone number, a channel), stored as sent and returned verbatim. The platform does not read the bag, so nothing in it reaches the model: a value the model should see belongs in `message`.",
                    "example": {
                      "phone": "5511999998888",
                      "channel": "whatsapp"
                    },
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/NullableMetadataBag"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "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.\nBackground by default: returns `202 Accepted` immediately and the reply\nlands as a new ConversationMessage when it completes — poll\n`GET /api/v1/conversations/{conversation_id}/messages` for it.\nPass `?wait=true` to block and receive the result inline. On\n`completed`, the reply is persisted as a new ConversationMessage\nauthored by that actor. On `requires_action`, nothing is persisted; the\ncaller must submit tool outputs via the Agents module and re-invoke\ngenerate — so a flow using client tools should pass `?wait=true`.\n",
        "operationId": "generateConversationMessage",
        "x-soat-resource": {
          "kind": "conversation",
          "from": "conversation_id"
        },
        "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 tool call, `builtin` or MCP, always waits.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "agent_id"
                ],
                "additionalProperties": false,
                "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"
                }
              }
            }
          },
          "429": {
            "description": "`QUOTA_EXCEEDED`: an `enforce`-mode generation quota is exhausted. `error.meta` carries `quota_id`, `metric`, `limit`, `window` and `resets_at`, with a `Retry-After` header. Only with `?wait=true`: a background call is answered `202` before the quota is read.",
            "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",
        "x-soat-resource": {
          "kind": "conversation",
          "from": "conversation_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "conversation",
          "from": "conversation_id"
        },
        "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": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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",
        "x-soat-resource": {
          "kind": "conversation",
          "from": "conversation_id"
        },
        "parameters": [
          {
            "name": "conversation_id",
            "in": "path",
            "required": true,
            "description": "Conversation ID",
            "schema": {
              "type": "string",
              "example": "conv_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tags replaced",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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 into the conversation's existing tags (existing tags are preserved unless overridden)",
        "operationId": "mergeConversationTags",
        "x-soat-resource": {
          "kind": "conversation",
          "from": "conversation_id"
        },
        "parameters": [
          {
            "name": "conversation_id",
            "in": "path",
            "required": true,
            "description": "Conversation ID",
            "schema": {
              "type": "string",
              "example": "conv_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tags merged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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/deciders": {
      "get": {
        "tags": [
          "Deciders"
        ],
        "summary": "List deciders",
        "description": "Returns the deciders defined in a project, newest first.",
        "operationId": "listDeciders",
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "description": "Project ID (required if not using project key auth)",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of deciders",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Decider"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "Deciders"
        ],
        "summary": "Create a decider",
        "description": "Creates a decider at version 1: a named question set and the backend that answers it, exactly one of `agent_id` and `tool_id`. Every question declares a finite answer space, validated here. The backend must be in the decider's project. An agent must carry no tool surface — no tool binding left active by `active_tool_ids` and no `knowledge_config.write_memory_store_id` — or the create is refused with `400 DECIDER_AGENT_NOT_TOOL_LESS`. A tool must be `http` or `pipeline` and must not pin `input` or `questions` in its `preset_parameters`, or the create is refused with `400 DECIDER_TOOL_NOT_CALLABLE`. A duplicate `name` in the project is `409 NAME_CONFLICT`.",
        "operationId": "createDecider",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateDeciderRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Decider created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Decider"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — invalid questions, an unknown or tool-bearing agent"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "409": {
            "description": "A decider with this name already exists in the project"
          }
        }
      }
    },
    "/api/v1/deciders/{decider_id}": {
      "get": {
        "tags": [
          "Deciders"
        ],
        "summary": "Get a decider",
        "description": "Returns a decider with its current question set and version.",
        "operationId": "getDecider",
        "x-soat-resource": {
          "kind": "decider",
          "from": "decider_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/DeciderId"
          }
        ],
        "responses": {
          "200": {
            "description": "The decider",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Decider"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Decider not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Deciders"
        ],
        "summary": "Update a decider",
        "description": "Updates any of `name`, `description`, the backend and `questions`. Naming `agent_id` or `tool_id` replaces the current backend; naming both is `400`. Only a change to `questions` archives a new version; a rename, a new backend or a rewrite of the questions the decider already holds leaves `version` where it is. A new backend is held to the same rules as on create.",
        "operationId": "updateDecider",
        "x-soat-resource": {
          "kind": "decider",
          "from": "decider_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/DeciderId"
          },
          {
            "$ref": "#/components/parameters/IfMatchVersion"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateDeciderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decider updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Decider"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — invalid questions, an unknown or tool-bearing agent"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Decider not found"
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      },
      "delete": {
        "tags": [
          "Deciders"
        ],
        "summary": "Delete a decider",
        "description": "Deletes a decider and its version archive. Its decisions remain and keep naming the decider's ID.",
        "operationId": "deleteDecider",
        "x-soat-resource": {
          "kind": "decider",
          "from": "decider_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/DeciderId"
          }
        ],
        "responses": {
          "204": {
            "description": "Decider deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Decider not found"
          }
        }
      }
    },
    "/api/v1/deciders/{decider_id}/versions": {
      "get": {
        "tags": [
          "Deciders"
        ],
        "summary": "List a decider's versions",
        "description": "Returns the decider's archived question sets, newest first. A version is written on create and on every write that changes `questions`.",
        "operationId": "listDeciderVersions",
        "x-soat-resource": {
          "kind": "decider",
          "from": "decider_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/DeciderId"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of decider versions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DeciderVersion"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Decider not found"
          }
        }
      }
    },
    "/api/v1/deciders/{decider_id}/versions/{version}": {
      "get": {
        "tags": [
          "Deciders"
        ],
        "summary": "Fetch an archived decider version",
        "description": "Returns the question set a version held. A decision names the `decider_version` it was answered under, so this is how its criteria are read after the decider has changed.",
        "operationId": "getDeciderVersion",
        "x-soat-resource": {
          "kind": "decider",
          "from": "decider_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/DeciderId"
          },
          {
            "$ref": "#/components/parameters/Version"
          }
        ],
        "responses": {
          "200": {
            "description": "Archived decider version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeciderVersion"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — version is not a positive integer"
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/deciders/{decider_id}/versions/{version}/restore": {
      "post": {
        "tags": [
          "Deciders"
        ],
        "summary": "Restore an archived decider version",
        "description": "Writes an archived question set back as the decider's live one, which archives it again as a new version rather than rewinding the counter. Restoring the question set the decider already holds is a no-op.",
        "operationId": "restoreDeciderVersion",
        "x-soat-resource": {
          "kind": "decider",
          "from": "decider_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/DeciderId"
          },
          {
            "$ref": "#/components/parameters/Version"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RestoreDeciderVersionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The decider, at its new version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Decider"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — version is not a positive integer"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      }
    },
    "/api/v1/deciders/{decider_id}/decisions": {
      "post": {
        "tags": [
          "Deciders"
        ],
        "summary": "Request a decision",
        "description": "Evaluates the decider against `input` and records the decision. The questions come from the decider, never from the request, so a call site can only supply what is judged. To send the questions with the request, use `POST /api/v1/decisions`.\n\nEverything that can refuse the request is checked before the decision is written, so a refusal is a `4xx` and never a polled failure: the agent's tool surface (`400 DECIDER_AGENT_NOT_TOOL_LESS`), a tool that cannot answer (`400 DECIDER_TOOL_NOT_CALLABLE`), a paused project (`409 PROJECT_PAUSED`) and, for an agent, quota admission (`429 QUOTA_EXCEEDED`).\n\nA tool backend is called with `{ input, questions }` and must answer `{ answers: [{ name, probability | choice | score, probabilities?, confidence? }] }`, the request and response of OpenAI's Decisions API; an answer outside that contract fails the decision with `DECISION_ANSWER_INVALID`.\n\nWith `wait: false`, the default, the answer is `201` with the decision `queued`; poll `GET /api/v1/decisions/{decision_id}` or subscribe to `decisions.completed` and `decisions.failed`. With `wait: true` it is `201` with the decision settled.",
        "operationId": "createDecision",
        "x-soat-resource": {
          "kind": "decider",
          "from": "decider_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/DeciderId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateDecisionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The decision — `queued`, or settled when `wait` is true",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Decision"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — a missing or malformed input, a non-object metadata bag, or a tool-bearing agent"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Decider not found"
          },
          "409": {
            "description": "The project is paused"
          },
          "429": {
            "description": "A generation quota is exhausted"
          }
        }
      }
    },
    "/api/v1/decisions": {
      "post": {
        "tags": [
          "Deciders"
        ],
        "summary": "Request a decision with inline questions",
        "description": "Evaluates `questions` sent with the request against `input`, answered by the agent or tool the request names (exactly one of `agent_id` and `tool_id`, held to the same rules as a decider's backend), and records the decision with its questions. The request and answers are those of OpenAI's Decisions API, with the backend in place of `model`.\n\nNo decider names the questions, so the decision carries them in `questions` and has no `decider_id` or `decider_version`. Admission, `wait` and the tool contract are those of `POST /api/v1/deciders/{decider_id}/decisions`.",
        "operationId": "createInlineDecision",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateInlineDecisionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The decision — `queued`, or settled when `wait` is true",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Decision"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — invalid questions or input, no backend or both, or a backend that cannot answer"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "409": {
            "description": "The project is paused"
          },
          "429": {
            "description": "A generation quota is exhausted"
          }
        }
      },
      "get": {
        "tags": [
          "Deciders"
        ],
        "summary": "List decisions",
        "description": "Returns the decisions in a project, newest first.",
        "operationId": "listDecisions",
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "description": "Project ID (required if not using project key auth)",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "decider_id",
            "in": "query",
            "description": "Only decisions requested from this decider",
            "schema": {
              "type": "string",
              "example": "dcd_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only decisions in these statuses. Repeat the parameter to OR values. A value outside the enum, empty string included, is a `400`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "queued",
                  "running",
                  "completed",
                  "failed"
                ]
              }
            },
            "style": "form",
            "explode": true
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of decisions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Decision"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      }
    },
    "/api/v1/decisions/{decision_id}": {
      "get": {
        "tags": [
          "Deciders"
        ],
        "summary": "Get a decision",
        "description": "Returns a decision. Poll it after a `wait: false` request until `status` is `completed` or `failed`.",
        "operationId": "getDecision",
        "x-soat-resource": {
          "kind": "decision",
          "from": "decision_id"
        },
        "parameters": [
          {
            "name": "decision_id",
            "in": "path",
            "required": true,
            "description": "The decision ID",
            "schema": {
              "type": "string",
              "example": "dec_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The decision",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Decision"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Decision not found"
          }
        }
      }
    },
    "/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": "include_withdrawn",
            "in": "query",
            "required": false,
            "description": "Include withdrawn documents. A withdrawn document leaves every default read and is not in the knowledge index at all — its chunks are dropped when it is withdrawn — so this shows it in the listing but never in a search.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "related_to",
            "in": "query",
            "description": "Only documents related to this one, on either side of the edge: what it points at, and what points at it. An id with no relations narrows the listing to nothing.",
            "schema": {
              "type": "string",
              "example": "doc_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/TagsQuery"
          },
          {
            "$ref": "#/components/parameters/MetadataQuery"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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"
                ],
                "additionalProperties": false,
                "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>`, or to `/<document_id>.txt` when neither is given — a document with no path is reachable only by its id, since a prefix filter never matches null.",
                    "example": "/reports/q1.txt"
                  },
                  "filename": {
                    "type": "string",
                    "example": "my-doc.txt"
                  },
                  "title": {
                    "type": "string",
                    "description": "Document title"
                  },
                  "metadata": {
                    "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.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/MetadataBag"
                      }
                    ]
                  },
                  "tags": {
                    "$ref": "#/components/schemas/TagBag"
                  },
                  "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"
                }
              }
            }
          },
          "409": {
            "description": "`path` is held by another file in the project (`NAME_CONFLICT`), or the project's `storage_bytes` quota is exceeded (`QUOTA_STORAGE_EXCEEDED`). Delete stored content, or raise the quota — no window reset clears a stored total, so no `Retry-After` is sent.",
            "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\nmore embedded chunks. The source format is detected from the file's content\ntype: PDFs are parsed page-by-page; `text/plain` and `text/markdown` files\nare read as a single source. How the source is chunked is controlled by\n`chunk_strategy`.\n\nA file can only back one Document — a second call with the same `file_id`\nreturns `409 FILE_ALREADY_INGESTED`. To re-process an already-ingested file\n(e.g. with a different `chunk_strategy`), use\n`POST /documents/{document_id}/ingest`; to ingest the same source under a\ndifferent path, upload a new copy of the file first.\n",
        "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"
                ],
                "additionalProperties": false,
                "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": {
                    "$ref": "#/components/schemas/TagBag"
                  },
                  "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
                  },
                  "session_id": {
                    "x-soat-ref": "sessions",
                    "type": "string",
                    "description": "Session the ingest runs for. The converter's generation and the chunk embeddings are metered against the session and its actor.",
                    "example": "sess_V1StGXR8Z5jdHi6B"
                  },
                  "actor_id": {
                    "x-soat-ref": "actors",
                    "type": "string",
                    "description": "Actor the ingest runs for when there is no session; metered like `session_id`. Beside `session_id` it must be that session's actor, else `400 VALIDATION_FAILED`.",
                    "example": "actor_V1StGXR8Z5jdHi6B"
                  }
                }
              }
            }
          }
        },
        "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, unsupported content type, or an unknown `session_id` (`SESSION_NOT_FOUND`) or `actor_id` (`ACTOR_NOT_FOUND`) in the project",
            "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), the path it would be filed at is held by another file in the project (`NAME_CONFLICT`), or the project's `storage_bytes` quota is exceeded (`QUOTA_STORAGE_EXCEEDED`; delete stored content or raise the quota — no `Retry-After` is sent, since no window reset clears a stored total).",
            "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/export": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "Export documents as NDJSON",
        "description": "Streams a project's documents as newline-delimited JSON — one document object per line, oldest first — for archiving the corpus or shipping it into another system. `project_id` is required: the export is per-project by design. The rows are the rows the listing returns for the same caller, so a policy that hides a document hides it here too, and withdrawn documents and the reserved `/.system/` root are left out.",
        "operationId": "exportDocuments",
        "x-soat-mcp-exclude": true,
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "required": true,
            "description": "Project whose documents are exported",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "path_prefix",
            "in": "query",
            "description": "Only documents filed under this directory. The prefix is a path boundary, not a substring, exactly as on the listing.",
            "schema": {
              "type": "string",
              "example": "/reports"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A newline-delimited stream of documents. Each line is a JSON object with the same fields as `Document`.",
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "`project_id` is required"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      }
    },
    "/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"
            }
          },
          {
            "$ref": "#/components/parameters/IfMatchVersion"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "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": {
                    "description": "Arbitrary metadata object, replacing the stored bag; `null` clears it. Unlike other body fields, keys are stored and returned verbatim in the casing supplied — they are not converted between snake_case and camelCase.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/NullableMetadataBag"
                      }
                    ]
                  },
                  "tags": {
                    "$ref": "#/components/schemas/TagBag"
                  },
                  "expected_version": {
                    "description": "Refuses the write unless the document is at this version.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/ExpectedVersion"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "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"
                }
              }
            }
          },
          "409": {
            "description": "The document has moved past the version this write read (`VERSION_CONFLICT`, with `meta.current_version`), or `path` is held by another file in the project (`NAME_CONFLICT`). Nothing is written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/documents/{document_id}/relations": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "List a document's relations",
        "description": "Returns the typed edges this document asserts about others, oldest first. An edge is owned by the document it leaves, so this is what the document claims — use `?related_to=` on the listing to find what claims something about it.",
        "operationId": "listDocumentRelations",
        "parameters": [
          {
            "name": "document_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "doc_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "The document's relations, oldest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DocumentRelation"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Document not found"
          }
        }
      },
      "post": {
        "tags": [
          "Documents"
        ],
        "summary": "Assert a relation",
        "description": "Asserts a typed edge from this document to another in the same project. Asserting the same edge twice is `409`: an edge is a fact, and the second assertion is the same fact.\n\nWriting an edge is a write of the asserting document (`documents:UpdateDocument`); the document it points at is unchanged, which is what lets an agent record what its own report derives from without being able to make another report claim anything.",
        "operationId": "createDocumentRelation",
        "parameters": [
          {
            "name": "document_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "doc_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "type",
                  "to_document_id"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "derived_from",
                      "supersedes",
                      "cites"
                    ],
                    "description": "What this document claims about the other",
                    "example": "cites"
                  },
                  "to_document_id": {
                    "type": "string",
                    "description": "The document the edge points at. Must be in the same project, and visible to the caller.",
                    "example": "doc_9Kp2mQxZ7bT4rN6W"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The asserted relation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentRelation"
                }
              }
            }
          },
          "400": {
            "description": "An undeclared `type`, a `to_document_id` that is not a document id, a document relating to itself, or a target in another project"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Either document was not found"
          },
          "409": {
            "description": "That relation is already asserted"
          }
        }
      }
    },
    "/api/v1/documents/{document_id}/relations/{relation_id}": {
      "delete": {
        "tags": [
          "Documents"
        ],
        "summary": "Retract a relation",
        "description": "Removes an edge this document asserts. Both documents are left as they are — retracting a claim is not a change to what it was about.",
        "operationId": "deleteDocumentRelation",
        "parameters": [
          {
            "name": "document_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "doc_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "relation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "doc_rel_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Relation retracted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Document or relation not found"
          }
        }
      }
    },
    "/api/v1/documents/{document_id}/status": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "Get document ingestion status",
        "description": "Returns a lightweight ingestion status payload for polling — `status`,\n`chunk_count`, `total_pages`, and (when failed) `error`. Unlike\n`GET /documents/{document_id}`, it never returns the assembled chunk\ncontent, so it is cheap to poll on large documents. A document whose\ningestion has stalled (no progress past the configured timeout) is\ntransitioned to `failed` with `error=INGESTION_TIMEOUT` on read.\n",
        "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}/withdraw": {
      "post": {
        "tags": [
          "Documents"
        ],
        "summary": "Withdraw a document",
        "description": "Takes a document out of every default read while keeping its history. The withdrawal is archived as a **tombstone version**, so one mechanism answers what a document holds now and there is no second lifecycle flag for a reader to miss.\n\nIts chunks are dropped from the knowledge index, so a withdrawn document costs a live search nothing. The content is not lost with them: it is in the version before the tombstone, which [`POST /api/v1/documents/{document_id}/versions/{version}/restore`](/docs/api/documents/restore-document-version) re-chunks from.\n\n`DELETE` stays what it is — permanent, and it removes the backing file. Withdrawal is the reversible act. It does not apply under `/.system/`: a platform-written document keeps the owning module's lifecycle.\n",
        "operationId": "withdrawDocument",
        "parameters": [
          {
            "name": "document_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IfMatchVersion"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "version_label": {
                    "type": "string",
                    "nullable": true,
                    "description": "Optional tag for the tombstone version this write archives.",
                    "example": "superseded-by-q2"
                  },
                  "expected_version": {
                    "description": "Refuses the withdrawal unless the document is at this version.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/ExpectedVersion"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The withdrawn document",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentRecord"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request — the document is filed under the reserved root",
            "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": "Document not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "The document is already withdrawn (`DOCUMENT_ALREADY_WITHDRAWN`), or it has moved past the version this write read (`VERSION_CONFLICT`, with `meta.current_version`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/documents/{document_id}/versions": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "List a document's content versions",
        "description": "Returns the document's archived states, newest first. A version is written on create and on every subsequent write that changes the content or its annotations, and a withdrawal is archived as a version carrying no content.\n",
        "operationId": "listDocumentVersions",
        "parameters": [
          {
            "name": "document_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of document versions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DocumentVersion"
                      }
                    },
                    "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": "Document not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/documents/{document_id}/versions/{version}": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "Fetch an archived document version",
        "description": "Returns the exact content and annotations the document held at a given version, which is what lets a run that cited a version read what it read.\n",
        "operationId": "getDocumentVersion",
        "parameters": [
          {
            "name": "document_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "version",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Archived document version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentVersion"
                }
              }
            }
          },
          "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/documents/{document_id}/versions/{version}/restore": {
      "post": {
        "tags": [
          "Documents"
        ],
        "summary": "Restore an archived document version",
        "description": "Writes an archived version's content and annotations back as the document's live state, which archives them again as a **new** version rather than rewinding the counter — so a run citing any version in between still resolves.\n\nThe restore runs through the ordinary update path, so the content is re-chunked and re-embedded; restoring the state the document already holds is a no-op and archives nothing. Restoring any content version of a withdrawn document brings it back into listings and search.\n\nA tombstone version has no content, so naming one is `400 VALIDATION_FAILED`: restore the version before it instead.\n",
        "operationId": "restoreDocumentVersion",
        "parameters": [
          {
            "name": "document_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": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "label": {
                    "type": "string",
                    "nullable": true,
                    "description": "Optional tag for the version this restore archives. Defaults to `restored from v<version>`.",
                    "example": "reinstated"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The document, at its new version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentRecord"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request — invalid version, or the version is a withdrawal",
            "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": "Document or version not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Another write took the document's version first (`VERSION_CONFLICT`, with `meta.current_version`), or the restored `path` is held by another file in the project (`NAME_CONFLICT`). Nothing is written.",
            "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\nsource file. Existing chunks are discarded and the document is reset to\n`status=pending` before re-processing. Use this to recover a document\nstuck in `processing`/`failed` or to re-chunk with a different strategy\nwithout re-uploading the file. Background by default (`202`); pass\n`?wait=true` to run synchronously (`201`).\n",
        "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",
                "additionalProperties": false,
                "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
                  },
                  "session_id": {
                    "x-soat-ref": "sessions",
                    "type": "string",
                    "description": "Session the ingest runs for. The converter's generation and the chunk embeddings are metered against the session and its actor.",
                    "example": "sess_V1StGXR8Z5jdHi6B"
                  },
                  "actor_id": {
                    "x-soat-ref": "actors",
                    "type": "string",
                    "description": "Actor the ingest runs for when there is no session; metered like `session_id`. Beside `session_id` it must be that session's actor, else `400 VALIDATION_FAILED`.",
                    "example": "actor_V1StGXR8Z5jdHi6B"
                  }
                }
              }
            }
          }
        },
        "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"
                }
              }
            }
          },
          "409": {
            "description": "The project's `storage_bytes` quota is exceeded (`QUOTA_STORAGE_EXCEEDED`). Delete stored content, or raise the quota — no window reset clears a stored total, so no `Retry-After` is sent.",
            "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\nreturning `{ \"status\": \"pending\" }` (see the Ingestion Rules module\ndocs). Not IAM-gated — the external converter is not a SOAT principal,\nso it authenticates with the single-use token minted for this\ndocument and ingestion attempt (delivered as `callback.token` /\nembedded in `callback.url` in the original converter invocation).\nAccepted only while the document is still awaiting that exact\nattempt; rejected with `409` if the attempt already completed, timed\nout, or was superseded by a re-ingest.\n",
        "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"
                    ],
                    "additionalProperties": false,
                    "properties": {
                      "text": {
                        "type": "string"
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": [
                      "pages"
                    ],
                    "additionalProperties": false,
                    "properties": {
                      "pages": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "additionalProperties": false,
                          "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": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tags replaced",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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 into the document's 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": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tags merged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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.\nProvide `input` for a single text or `inputs` for a batch. At least one is required.\nReturns `embedding` when `input` is used, and `embeddings` when `inputs` is used.\n\n`project_id` names the project the call's token usage is metered against, so\nembedding spend reaches the usage rollup and the project's `cost_usd` / `tokens`\nquotas. A project-scoped credential supplies its own project; a call that names\nnone and is bound to none is `400 VALIDATION_FAILED`.\n",
        "operationId": "createEmbeddings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "project_id": {
                    "type": "string",
                    "description": "Project the call's usage is billed to. Required unless the credential is bound to a project, which then supplies its own. Requires write access to the named project.",
                    "example": "proj_V1StGXR8Z5jdHi6B"
                  },
                  "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. At most 256 per request — each input is one call to the embedding model, so a larger batch is refused (`VALIDATION_FAILED`) rather than queued. Split it across requests.",
                    "maxItems": 256,
                    "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, or no project to bill (no `project_id` and a credential bound to none)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The caller cannot write to the named `project_id`",
            "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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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.\n\nDatasets 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"
                ],
                "additionalProperties": false,
                "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",
        "x-soat-resource": {
          "kind": "dataset",
          "from": "dataset_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "dataset",
          "from": "dataset_id"
        },
        "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",
                "additionalProperties": false,
                "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",
        "x-soat-resource": {
          "kind": "dataset",
          "from": "dataset_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "dataset",
          "from": "dataset_id"
        },
        "parameters": [
          {
            "name": "dataset_id",
            "in": "path",
            "required": true,
            "description": "Dataset ID",
            "schema": {
              "type": "string",
              "example": "dset_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
        "x-soat-resource": {
          "kind": "dataset",
          "from": "dataset_id"
        },
        "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"
                ],
                "additionalProperties": false,
                "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": {
                    "description": "Free-form tags, opaque to the platform",
                    "example": {
                      "topic": "billing"
                    },
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/NullableMetadataBag"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "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"
          },
          "409": {
            "description": "The project's `storage_bytes` quota is exceeded (`QUOTA_STORAGE_EXCEEDED`). A fixture is a corpus write like a document, so it is bounded by the same cap; delete stored content or raise the quota — no `Retry-After` is sent, since no window reset clears a stored total."
          }
        }
      }
    },
    "/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.\n\nThe 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.\n\nRequires 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.\n\nOnly 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",
        "x-soat-resource": {
          "kind": "dataset",
          "from": "dataset_id"
        },
        "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"
                ],
                "additionalProperties": false,
                "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": {
                    "description": "Free-form tags, opaque to the platform",
                    "example": {
                      "topic": "billing"
                    },
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/NullableMetadataBag"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "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, its content was never stored or has been purged, or the project's `storage_bytes` quota is exceeded (`QUOTA_STORAGE_EXCEEDED`; delete stored content or raise the quota — no `Retry-After` is sent, since no window reset clears a stored total)"
          }
        }
      }
    },
    "/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",
        "x-soat-resource": {
          "kind": "dataset",
          "from": "dataset_id"
        },
        "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",
                "additionalProperties": false,
                "properties": {
                  "input": {
                    "$ref": "#/components/schemas/DatasetItemInput"
                  },
                  "expected_output": {
                    "type": "string",
                    "nullable": true,
                    "example": "Your invoice is issued on the first of each month."
                  },
                  "metadata": {
                    "example": {
                      "topic": "billing"
                    },
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/NullableMetadataBag"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "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",
        "x-soat-resource": {
          "kind": "dataset",
          "from": "dataset_id"
        },
        "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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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.\n\nScorer 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"
                ],
                "additionalProperties": false,
                "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
                  },
                  "group_by": {
                    "type": "string",
                    "nullable": true,
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "A key of the items' `metadata`. A run rolls its scores up per string value of that key in `aggregate_scores.grouping`. Null reports no grouping.",
                    "example": "kind"
                  }
                }
              }
            }
          }
        },
        "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",
        "x-soat-resource": {
          "kind": "eval",
          "from": "eval_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "eval",
          "from": "eval_id"
        },
        "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",
                "additionalProperties": false,
                "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
                  },
                  "group_by": {
                    "type": "string",
                    "nullable": true,
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "A key of the items' `metadata` to group run scores by; null clears it, omitting it leaves it unchanged.",
                    "example": "kind"
                  }
                }
              }
            }
          }
        },
        "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",
        "x-soat-resource": {
          "kind": "eval",
          "from": "eval_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "eval",
          "from": "eval_id"
        },
        "parameters": [
          {
            "name": "eval_id",
            "in": "path",
            "required": true,
            "description": "Eval ID",
            "schema": {
              "type": "string",
              "example": "eval_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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.\n\n`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.\n\n`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_runs.completed` webhook. There is no item cap on a queued run.\n\nThe 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.\n\nWith `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",
        "x-soat-resource": {
          "kind": "eval",
          "from": "eval_id"
        },
        "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",
                "additionalProperties": false,
                "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": {
                    "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.\n\nThe 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"
                    },
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/MetadataBag"
                      }
                    ]
                  },
                  "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`.\n\nStored 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.\n\nAn 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",
        "x-soat-resource": {
          "kind": "eval",
          "from": "eval_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "eval",
          "from": "eval_id"
        },
        "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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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 `cancelled`.\n\nResults 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.\n\nA run that has already finished is rejected with 400.",
        "operationId": "cancelEvalRun",
        "x-soat-resource": {
          "kind": "eval",
          "from": "eval_id"
        },
        "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 cancelled",
            "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",
            "required": false,
            "description": "Filter by triage status. Repeat the parameter to OR values. A value outside the enum, empty string included, is a `400`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "open",
                  "acknowledged",
                  "resolved"
                ]
              }
            },
            "style": "form",
            "explode": true
          },
          {
            "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"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
                "additionalProperties": false,
                "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": "path_prefix",
            "in": "query",
            "required": false,
            "description": "Only files 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. Naming a directory under `/.system/` is what includes platform-written files, which a list without this parameter leaves out.",
            "schema": {
              "type": "string",
              "example": "/reports/"
            }
          },
          {
            "$ref": "#/components/parameters/TagsQuery"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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": {
                    "$ref": "#/components/schemas/MetadataBag"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "File created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileRecord"
                }
              }
            }
          },
          "409": {
            "description": "The project's `storage_bytes` quota is exceeded (`QUOTA_STORAGE_EXCEEDED`). Delete stored content, or raise the quota — no window reset clears a stored total, so no `Retry-After` is sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "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": {
                    "$ref": "#/components/schemas/MetadataBagText"
                  }
                }
              }
            }
          }
        },
        "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"
          },
          "409": {
            "description": "The project's `storage_bytes` quota is exceeded (`QUOTA_STORAGE_EXCEEDED`). Delete stored content, or raise the quota — no window reset clears a stored total, so no `Retry-After` is sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "The upload is over this deployment's byte ceiling (`UPLOAD_TOO_LARGE`; `FILE_UPLOAD_MAX_BYTES`, 25 MB by default). The request is refused while the body is still streaming, so nothing was stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/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"
          },
          "409": {
            "description": "The project's `storage_bytes` quota is exceeded (`QUOTA_STORAGE_EXCEEDED`). Delete stored content, or raise the quota — no window reset clears a stored total, so no `Retry-After` is sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/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": {
                    "$ref": "#/components/schemas/MetadataBagText"
                  }
                }
              }
            },
            "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, or the project's `storage_bytes` quota is exceeded (`QUOTA_STORAGE_EXCEEDED`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "Upload token expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "The upload is over this deployment's byte ceiling (`UPLOAD_TOO_LARGE`; `FILE_UPLOAD_MAX_BYTES`, 25 MB by default). The request is refused while the body is still streaming, so nothing was stored.",
            "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"
            }
          },
          {
            "name": "token",
            "in": "query",
            "required": false,
            "description": "Signed single-file download token, an alternative to a bearer credential (issued for ingestion-rule converters).",
            "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",
                "additionalProperties": false,
                "properties": {
                  "metadata": {
                    "description": "Replaces the stored bag; `null` clears it.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/NullableMetadataBag"
                      }
                    ]
                  },
                  "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"
            }
          },
          {
            "name": "token",
            "in": "query",
            "required": false,
            "description": "Signed single-file download token, an alternative to a bearer credential (issued for ingestion-rule converters).",
            "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": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tags replaced",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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 into the file's existing tags (existing tags are preserved unless overridden)",
        "operationId": "mergeFileTags",
        "parameters": [
          {
            "name": "file_id",
            "in": "path",
            "required": true,
            "description": "File ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tags merged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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.\n",
        "operationId": "validateFormation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "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.\n",
                    "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.\n",
        "operationId": "planFormation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "template"
                ],
                "additionalProperties": false,
                "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.\n",
                    "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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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.\n\nA **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.\n",
        "operationId": "createFormation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "template"
                ],
                "additionalProperties": false,
                "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.\n",
                    "nullable": true
                  },
                  "metadata": {
                    "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`.\n",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/NullableMetadataBag"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "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.\n\nA **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.\n\nA 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.\n",
        "operationId": "updateFormation",
        "parameters": [
          {
            "name": "formation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "form_V1StGXR8Z5jdHi6B"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "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.\n",
                    "nullable": true
                  },
                  "metadata": {
                    "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`.\n",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/NullableMetadataBag"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "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.\n\nA 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.\n\nA 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.\n",
        "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`).\n"
          }
        }
      }
    },
    "/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.\n",
        "operationId": "listFormationEvents",
        "parameters": [
          {
            "name": "formation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "form_V1StGXR8Z5jdHi6B"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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. The generations of one trace are the `trace_id` filter.\n\nFiltering 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.\n",
        "operationId": "listGenerations",
        "parameters": [
          {
            "name": "agent_id",
            "in": "query",
            "required": false,
            "description": "Filter by agent public ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "trace_id",
            "in": "query",
            "required": false,
            "description": "Filter by trace public ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "session_id",
            "in": "query",
            "required": false,
            "description": "Return only the generations dispatched through one session — the turns behind that conversation's spend. A session that does not exist in scope yields an empty page.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actor_id",
            "in": "query",
            "required": false,
            "description": "Return only the generations one end user started, across every session they appear in. An actor that does not exist in scope yields an empty page.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "initiator_generation_id",
            "in": "query",
            "required": false,
            "description": "Filter by the public ID of the parent generation. Returns every generation started by that generation: sub-agent invocations, approval continuations, client-tool re-handoffs and memory-rule handler turns. Top-level generations are not returned.\n",
            "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.\n",
            "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.\n",
            "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`.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by lifecycle status. Repeat the parameter to OR values. A value outside the enum, empty string included, is a `400`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "running",
                  "requires_action",
                  "completed",
                  "failed"
                ]
              }
            },
            "style": "form",
            "explode": true
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of generations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Generation"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/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).\n",
        "operationId": "getGeneration",
        "x-soat-resource": {
          "kind": "generation",
          "from": "generation_id"
        },
        "parameters": [
          {
            "name": "generation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the generation"
          }
        ],
        "responses": {
          "200": {
            "description": "Generation details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Generation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/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. the ticket or case an AI action belongs to). The provided keys are shallow-merged over the existing `metadata`, so repeated patches accumulate. The bag is caller-owned and no key is reserved: server-owned state (usage attribution, the served agent version, the route's record, the extraction summary, what knowledge retrieval served) lives in its own top-level fields and cannot be written from here.\n",
        "operationId": "updateGeneration",
        "x-soat-resource": {
          "kind": "generation",
          "from": "generation_id"
        },
        "parameters": [
          {
            "name": "generation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the generation"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateGenerationRequest"
              },
              "examples": {
                "audit": {
                  "summary": "Attach caller audit metadata",
                  "value": {
                    "metadata": {
                      "team": "payments",
                      "ticket_id": "OPS-4821"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated generation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Generation"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request (e.g. metadata is not a JSON object)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/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`.\n\nThe 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.\n\nThis 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.\n\nIdempotent — purging an already-purged generation succeeds and leaves the original `content_redacted_at` in place.\n",
        "operationId": "purgeGenerationContent",
        "x-soat-resource": {
          "kind": "generation",
          "from": "generation_id"
        },
        "parameters": [
          {
            "name": "generation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the generation"
          }
        ],
        "responses": {
          "200": {
            "description": "The purged generation skeleton",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Generation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/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}/memory-assertions": {
      "get": {
        "tags": [
          "Generations"
        ],
        "summary": "List the memory writes a generation made",
        "description": "Every memory write this turn made, oldest first: each memory rule's firings and the agent's own `write_memory` calls alike. The rows behind the generation's per-rule `extraction` counts. Reading them is reading the generation (`generations:GetGeneration`).\n",
        "operationId": "listGenerationMemoryAssertions",
        "x-soat-resource": {
          "kind": "generation",
          "from": "generation_id"
        },
        "parameters": [
          {
            "name": "generation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the generation"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "The generation's memory writes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MemoryAssertion"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "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.\n\nThe 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.\n\nA 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.\n",
        "operationId": "getGenerationTranscript",
        "x-soat-resource": {
          "kind": "generation",
          "from": "generation_id"
        },
        "parameters": [
          {
            "name": "generation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the generation"
          }
        ],
        "responses": {
          "200": {
            "description": "The generation's transcript",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GenerationTranscript"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/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.\n",
        "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.projects.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"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
        "x-soat-resource": {
          "kind": "guardrail",
          "from": "guardrail_id"
        },
        "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.\n",
        "operationId": "updateGuardrail",
        "x-soat-resource": {
          "kind": "guardrail",
          "from": "guardrail_id"
        },
        "parameters": [
          {
            "name": "guardrail_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IfMatchVersion"
          }
        ],
        "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"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      },
      "delete": {
        "tags": [
          "Guardrails"
        ],
        "summary": "Delete a guardrail",
        "description": "Deletes a guardrail and its archived versions by ID.",
        "operationId": "deleteGuardrail",
        "x-soat-resource": {
          "kind": "guardrail",
          "from": "guardrail_id"
        },
        "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).\n",
        "operationId": "listGuardrailVersions",
        "x-soat-resource": {
          "kind": "guardrail",
          "from": "guardrail_id"
        },
        "parameters": [
          {
            "name": "guardrail_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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.\n",
        "operationId": "getGuardrailVersion",
        "x-soat-resource": {
          "kind": "guardrail",
          "from": "guardrail_id"
        },
        "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.\n\nThe 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.\n",
        "operationId": "restoreGuardrailVersion",
        "x-soat-resource": {
          "kind": "guardrail",
          "from": "guardrail_id"
        },
        "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"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      }
    },
    "/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.tools.*`.\n",
        "operationId": "evaluateGuardrail",
        "x-soat-resource": {
          "kind": "guardrail",
          "from": "guardrail_id"
        },
        "parameters": [
          {
            "name": "guardrail_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "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`.\n"
                  },
                  "tool_id": {
                    "type": "string",
                    "description": "Optional tool to resolve `runtime.tools.*` 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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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"
                ],
                "additionalProperties": false,
                "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. At most 4 wildcards and 255 characters — a MIME glob needs one on each side of the slash at most.",
                    "maxLength": 255,
                    "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": {
                    "description": "Arbitrary JSON metadata",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/MetadataBag"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "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",
        "x-soat-resource": {
          "kind": "ingestionRule",
          "from": "ingestion_rule_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "ingestionRule",
          "from": "ingestion_rule_id"
        },
        "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",
                "additionalProperties": false,
                "properties": {
                  "content_type_glob": {
                    "type": "string",
                    "description": "MIME type glob matched against a file's content_type. At most 4 wildcards and 255 characters.",
                    "maxLength": 255
                  },
                  "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": {
                    "$ref": "#/components/schemas/NullableMetadataBag"
                  }
                }
              }
            }
          }
        },
        "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",
        "x-soat-resource": {
          "kind": "ingestionRule",
          "from": "ingestion_rule_id"
        },
        "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 memories using hybrid retrieval — a vector query and a full-text query per source, fused by reciprocal rank — or by file paths, document IDs, memory store IDs, or tags. At least one of `query`, `tags`, `document_paths`, `document_ids`, or `memory_store_ids` must be provided.",
        "operationId": "searchKnowledge",
        "x-iam-action": "knowledge:SearchKnowledge",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "project_id": {
                    "x-soat-ref": "projects",
                    "type": "string",
                    "description": "Limit search to a specific project",
                    "example": "proj_V1StGXR8Z5jdHi6B"
                  },
                  "query": {
                    "type": "string",
                    "description": "Search query text. Runs a vector query and a full-text query over each store and fuses the rankings; the full-text channel matches exact tokens and phrases, the vector channel everything else.",
                    "example": "customer communication preferences"
                  },
                  "min_similarity": {
                    "type": "number",
                    "description": "Minimum raw cosine `similarity_score` a **vector** candidate must reach to take part in ranking, applied before fusion. Only applies when `query` is provided. Lexical candidates are deliberately exempt: a result that literally contains the searched token is the evidence, and dropping it for a low cosine is the failure hybrid search exists to prevent. Not a floor on `score`, whose fused value encodes rank position rather than similarity.",
                    "minimum": 0,
                    "maximum": 1,
                    "example": 0.5
                  },
                  "rrf_k": {
                    "type": "integer",
                    "description": "The `k` in the reciprocal rank fusion term `1 / (k + rank)`, which sets how steeply a result's contribution decays with its position in each ranked list. A smaller value weights the very top of each list more heavily. Defaults to the deployment's `KNOWLEDGE_RRF_K`, itself 60 by default. Only applies when `query` is provided.",
                    "minimum": 1,
                    "example": 60
                  },
                  "recency_half_life_days": {
                    "type": "number",
                    "description": "Half-life, in days, of a recency decay applied to **memory store** results after fusion: a result's `score` is multiplied by `2 ^ (-age_in_days / recency_half_life_days)`, where age is measured from its `updated_at`. Document results are never decayed. `0` — the default, and the default of the deployment's `KNOWLEDGE_RECENCY_HALF_LIFE_DAYS` — disables the blend entirely; it is a switch, not a lower bound, and sending `0` turns off a deployment-wide decay for this one request. Accepts fractions (`0.5` is twelve hours). How many ranks a given half-life costs depends on `rrf_k`. Only applies when `query` is provided. **No value is known to be safe in general**: every half-life measured against the reference corpus that lifted recency-sensitive queries also cost relevance-sensitive ones, which is why this ships disabled. Measure against your own corpus before setting it — see the Retrieval Quality guide.",
                    "minimum": 0,
                    "example": 30
                  },
                  "limit": {
                    "type": "integer",
                    "description": "Maximum number of results to return (default 10). A value above 100 is clamped to 100 — the ceiling bounds the vector scan this one request performs, so a larger `limit` returns everything there is up to that many rows rather than being refused.",
                    "minimum": 1,
                    "example": 10
                  },
                  "include_documents": {
                    "type": "boolean",
                    "default": true,
                    "description": "Set `false` to leave the document store out of this search. A `query` names no store, so it reaches both; the store-specific filters narrow *within* a store rather than choosing between them. This is the switch, and a filter naming the other store never overrides it. `false` for both stores is `400`.",
                    "example": false
                  },
                  "include_memories": {
                    "type": "boolean",
                    "default": true,
                    "description": "Set `false` to leave the memory store out of this search. The mirror of `include_documents`, for a caller that wants documents alone.",
                    "example": false
                  },
                  "memory_store_ids": {
                    "x-soat-ref": "memory-stores",
                    "type": "array",
                    "description": "Search memories within these specific memory stores",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "mstore_V1StGXR8Z5jdHi6B"
                    ]
                  },
                  "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"
                    ]
                  },
                  "tags": {
                    "description": "Filter results to documents and memories whose `tags` contain every one of these key-value pairs (exact, case-sensitive match). Scopes both stores, so passing it alone searches both — as `query` does. For memories it matches at memory granularity: a memory is returned when its parent memory store's tags match or its own do.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/TagBag"
                      }
                    ]
                  },
                  "metadata": {
                    "description": "Filter document results by their `metadata` bag. A document-store filter: a memory carries no such bag, so passing it alone searches documents, as `document_paths` does.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/MetadataFilter"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "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, or a `query` and no project in scope the caller holds `embeddings:CreateEmbeddings` on to bill its embedding to",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/memories": {
      "get": {
        "tags": [
          "Memories"
        ],
        "summary": "List memories",
        "description": "Returns all memories in a memory store",
        "operationId": "listMemories",
        "x-iam-action": "memories:ListMemories",
        "parameters": [
          {
            "name": "memory_store_id",
            "in": "query",
            "required": true,
            "description": "Memory store to list memories from (mstore_...)",
            "schema": {
              "type": "string",
              "example": "mstore_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/TagsQuery"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "name": "include_invalidated",
            "in": "query",
            "required": false,
            "description": "Include invalidated memories — superseded or retracted. They are excluded by default; set this to audit what a store once held.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "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"
          },
          "404": {
            "description": "Memory store not found"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "post": {
        "tags": [
          "Memories"
        ],
        "summary": "Create a memory",
        "description": "Writes a fact to the specified memory store through the standard write algorithm: the content is embedded (or matched to text the store already holds), compared against the most similar currently-valid memory, and resolved to exactly one of three outcomes — `skipped` at or above `duplicate_threshold`, `superseded` at or above `supersede_threshold`, `created` below it. `supersedes` overrides that comparison entirely, retiring the memory it names. Every call records one assertion, whatever the outcome.",
        "operationId": "createMemory",
        "x-soat-resource": {
          "kind": "memory_store",
          "from": "memory_store_id"
        },
        "x-iam-action": "memories:CreateMemory",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "memory_store_id",
                  "content"
                ],
                "additionalProperties": false,
                "properties": {
                  "memory_store_id": {
                    "x-soat-ref": "memory-stores",
                    "type": "string",
                    "description": "Memory store to add the memory to (mstore_...)",
                    "example": "mstore_V1StGXR8Z5jdHi6B"
                  },
                  "content": {
                    "type": "string",
                    "description": "The text content of the memory",
                    "example": "The customer prefers email communication over phone calls"
                  },
                  "source_type": {
                    "type": "string",
                    "enum": [
                      "manual",
                      "conversation"
                    ],
                    "description": "Whether there is a source to point at. `conversation` requires `source_id`; `manual` rejects it.",
                    "default": "manual",
                    "example": "manual"
                  },
                  "source_id": {
                    "type": "string",
                    "description": "The conversation this fact was learned in. Required when `source_type` is `conversation`, and rejected otherwise.",
                    "example": "conv_V1StGXR8Z5jdHi6B"
                  },
                  "tags": {
                    "description": "Per-memory key-value tags, used for memory-granularity filtering by `tags` in search-knowledge.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/TagBag"
                      }
                    ]
                  },
                  "metadata": {
                    "description": "Arbitrary structured metadata attached to the memory",
                    "example": {
                      "evidence": "high",
                      "quarter": "Q3"
                    },
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/MetadataBag"
                      }
                    ]
                  },
                  "supersedes": {
                    "type": "string",
                    "description": "The memory this write replaces, named outright. The declaration outranks the thresholds in both directions: the target is retired and the write returns `superseded` however similar or distant the two texts are. This is what reaches a contradiction cosine cannot see (\"The office is in Lisbon\" then \"We closed the Lisbon office\"). The target must be a still-valid memory in the same memory store, and the caller needs `memories:UpdateMemory` on it as well as `memories:CreateMemory` on the store.",
                    "example": "mem_V1StGXR8Z5jdHi6B"
                  },
                  "duplicate_threshold": {
                    "type": "number",
                    "description": "Cosine similarity at or above which the incoming content is a duplicate of an existing memory and the write is skipped. Overrides the store's `duplicate_threshold` for this call; falls back to the store's value, then to `0.95`.",
                    "minimum": 0,
                    "maximum": 1,
                    "example": 0.95
                  },
                  "supersede_threshold": {
                    "type": "number",
                    "description": "Cosine similarity at or above which the incoming content restates a fact that has changed: the top match is invalidated and a new memory replaces it. Overrides the store's `supersede_threshold` for this call; falls back to the store's value, then to `0.90`. The effective pair must satisfy `supersede_threshold < duplicate_threshold`, and the check runs against the effective pair — so overriding only one value cannot invert it against the store's other one.",
                    "minimum": 0,
                    "maximum": 1,
                    "example": 0.9
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The write resolved against an existing memory — `action` is `skipped` (the fact was already known) or `superseded` (the fact had changed, and the returned memory is the replacement).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemoryWriteResult"
                }
              }
            }
          },
          "201": {
            "description": "Memory created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemoryWriteResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — a missing required field, a threshold pair whose effective values are not `supersede_threshold < duplicate_threshold`, or a `supersedes` that is not a memory id, or names a memory in another memory store or one already superseded."
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — the caller may not write to the memory store, or may not update the memory named by `supersedes`."
          },
          "404": {
            "description": "Memory store not found, or no memory matches `supersedes`"
          },
          "409": {
            "description": "The project's `storage_bytes` quota is exceeded (`QUOTA_STORAGE_EXCEEDED`). Delete stored content, or raise the quota — no window reset clears a stored total, so no `Retry-After` is sent."
          },
          "500": {
            "description": "Internal server error"
          }
        }
      }
    },
    "/api/v1/memories/{memory_id}": {
      "get": {
        "tags": [
          "Memories"
        ],
        "summary": "Get a memory",
        "description": "Returns a single memory by ID",
        "operationId": "getMemory",
        "x-soat-resource": {
          "kind": "memory",
          "from": "memory_id"
        },
        "x-iam-action": "memories: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 store or memory not found"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "put": {
        "tags": [
          "Memories"
        ],
        "summary": "Update a memory",
        "description": "Updates an existing memory. Regenerates the embedding if content changes.",
        "operationId": "updateMemory",
        "x-soat-resource": {
          "kind": "memory",
          "from": "memory_id"
        },
        "x-iam-action": "memories:UpdateMemory",
        "parameters": [
          {
            "name": "memory_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "mem_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/IfMatchVersion"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "content": {
                    "type": "string",
                    "description": "Updated text content"
                  },
                  "tags": {
                    "description": "Replaces the memory's tags. Pass null or an empty object to clear.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/NullableTagBag"
                      }
                    ]
                  },
                  "metadata": {
                    "description": "Replaces the memory's metadata. Pass null to clear.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/NullableMetadataBag"
                      }
                    ]
                  },
                  "expected_version": {
                    "description": "Refuses the write unless the memory is at this version.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/ExpectedVersion"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Memory updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Memory"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store or memory not found"
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "delete": {
        "tags": [
          "Memories"
        ],
        "summary": "Delete a memory",
        "description": "Deletes a memory",
        "operationId": "deleteMemory",
        "x-soat-resource": {
          "kind": "memory",
          "from": "memory_id"
        },
        "x-iam-action": "memories: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 store or memory not found"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      }
    },
    "/api/v1/memories/{memory_id}/retract": {
      "post": {
        "tags": [
          "Memories"
        ],
        "summary": "Retract a memory",
        "description": "Retires a fact that stopped holding with nothing replacing it. The memory leaves the default listing, [knowledge search](/docs/api/knowledge/search-knowledge) and write deduplication, so restating the fact later lands as a new memory.\n\nIt is an invalidation with no successor, which is what tells it apart from a supersede: `invalidated_at` is set and `superseded_by_memory_id` stays null. The retraction is appended to the assertion ledger with outcome `retracted`, so who withdrew the fact is part of the record.\n\nThe memory stays readable by id, with its text and its assertions. `DELETE` remains the way to remove it outright.\n",
        "operationId": "retractMemory",
        "x-soat-resource": {
          "kind": "memory",
          "from": "memory_id"
        },
        "x-iam-action": "memories:RetractMemory",
        "parameters": [
          {
            "name": "memory_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "mem_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/IfMatchVersion"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "expected_version": {
                    "description": "Refuses the retraction unless the memory is at this version.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/ExpectedVersion"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The retracted memory",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Memory"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store or memory not found"
          },
          "409": {
            "description": "The memory is already invalidated (`MEMORY_ALREADY_INVALIDATED`), or it has moved past the version this write read (`VERSION_CONFLICT`, with `meta.current_version`)."
          },
          "500": {
            "description": "Internal server error"
          }
        }
      }
    },
    "/api/v1/memories/{memory_id}/assertions": {
      "get": {
        "tags": [
          "Memories"
        ],
        "summary": "List a memory's assertions",
        "description": "Returns every write that resolved into this memory, oldest first: the one that created it, the duplicates it absorbed, and the assertion that superseded another memory in its favour. A superseded memory keeps its own assertions, so the chain can be walked in both directions.",
        "operationId": "listMemoryAssertions",
        "x-soat-resource": {
          "kind": "memory",
          "from": "memory_id"
        },
        "x-iam-action": "memories:ListMemoryAssertions",
        "parameters": [
          {
            "name": "memory_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "mem_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of assertions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MemoryAssertion"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store or memory not found"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      }
    },
    "/api/v1/memories/{memory_id}/tags": {
      "get": {
        "tags": [
          "Memories"
        ],
        "summary": "Get memory tags",
        "description": "Returns all tags attached to the memory",
        "operationId": "getMemoryTags",
        "x-soat-resource": {
          "kind": "memory",
          "from": "memory_id"
        },
        "x-iam-action": "memories:GetMemory",
        "parameters": [
          {
            "name": "memory_id",
            "in": "path",
            "required": true,
            "description": "Memory ID",
            "schema": {
              "type": "string",
              "example": "mem_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Memory tags",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store or memory not found"
          }
        }
      },
      "put": {
        "tags": [
          "Memories"
        ],
        "summary": "Replace memory tags",
        "description": "Replaces all tags on the memory with the provided tags (not merged)",
        "operationId": "replaceMemoryTags",
        "x-soat-resource": {
          "kind": "memory",
          "from": "memory_id"
        },
        "x-iam-action": "memories:UpdateMemory",
        "parameters": [
          {
            "name": "memory_id",
            "in": "path",
            "required": true,
            "description": "Memory ID",
            "schema": {
              "type": "string",
              "example": "mem_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tags replaced",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "400": {
            "description": "Body is not an object of string values"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store or memory not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Memories"
        ],
        "summary": "Merge memory tags",
        "description": "Merges provided tags into the memory's existing tags (existing tags are preserved unless overridden)",
        "operationId": "mergeMemoryTags",
        "x-soat-resource": {
          "kind": "memory",
          "from": "memory_id"
        },
        "x-iam-action": "memories:UpdateMemory",
        "parameters": [
          {
            "name": "memory_id",
            "in": "path",
            "required": true,
            "description": "Memory ID",
            "schema": {
              "type": "string",
              "example": "mem_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tags merged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "400": {
            "description": "Body is not an object of string values"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store or memory not found"
          }
        }
      }
    },
    "/api/v1/memory-rules": {
      "get": {
        "tags": [
          "Memory Rules"
        ],
        "summary": "List memory rules",
        "description": "Returns memory rules, newest first. Narrow to one store with `memory_store_id` — that form is authorized against the store itself, so it answers \"what feeds this store?\" in one call.",
        "operationId": "listMemoryRules",
        "x-iam-action": "memories:ListMemoryRules",
        "parameters": [
          {
            "name": "memory_store_id",
            "in": "query",
            "description": "Only the rules of this memory store",
            "schema": {
              "type": "string",
              "example": "mstore_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "description": "Project ID (required if not using project key auth)",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of memory rules",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MemoryRule"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "post": {
        "tags": [
          "Memory Rules"
        ],
        "summary": "Create a memory rule",
        "description": "Creates an ingestion rule on a memory store. With neither `agent_id` nor `tool_id` the built-in extractor runs, configurable through `prompt`, `ai_provider_id` and `model`.",
        "operationId": "createMemoryRule",
        "x-soat-resource": {
          "kind": "memory_store",
          "from": "memory_store_id"
        },
        "x-iam-action": "memories:CreateMemoryRule",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "memory_store_id",
                  "on"
                ],
                "additionalProperties": false,
                "properties": {
                  "memory_store_id": {
                    "x-soat-ref": "memory-stores",
                    "type": "string",
                    "description": "The destination store, and the rule's owning scope",
                    "example": "mstore_V1StGXR8Z5jdHi6B"
                  },
                  "on": {
                    "$ref": "#/components/schemas/MemoryRuleEvent"
                  },
                  "source_agent_ids": {
                    "type": "array",
                    "nullable": true,
                    "description": "Agents whose turns this rule reads. Omit or send `null` for every agent in the store's project.",
                    "items": {
                      "x-soat-ref": "agents",
                      "type": "string"
                    },
                    "example": [
                      "agent_V1StGXR8Z5jdHi6B"
                    ]
                  },
                  "agent_id": {
                    "x-soat-ref": "agents",
                    "type": "string",
                    "nullable": true,
                    "description": "Handler agent (mutually exclusive with tool_id)"
                  },
                  "tool_id": {
                    "x-soat-ref": "tools",
                    "type": "string",
                    "nullable": true,
                    "description": "Handler tool (mutually exclusive with agent_id)"
                  },
                  "action": {
                    "type": "string",
                    "nullable": true,
                    "description": "Operation id, for a tool handler"
                  },
                  "preset_parameters": {
                    "type": "object",
                    "nullable": true,
                    "description": "Merged into a tool handler's input before invocation. The turn's own fields are reserved and win."
                  },
                  "prompt": {
                    "type": "string",
                    "nullable": true,
                    "description": "Replaces the built-in extractor's task instructions. The JSON response contract and the transcript are always appended. Not valid with a handler."
                  },
                  "ai_provider_id": {
                    "x-soat-ref": "ai-providers",
                    "type": "string",
                    "nullable": true,
                    "description": "Provider override for the built-in extractor's completion. Not valid with a handler."
                  },
                  "model": {
                    "type": "string",
                    "nullable": true,
                    "description": "Model override for the built-in extractor's completion. Not valid with a handler."
                  },
                  "enabled": {
                    "type": "boolean",
                    "default": true,
                    "description": "A disabled rule is kept and never fires"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Memory rule created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemoryRule"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed (e.g. agent_id and tool_id both set, or an extractor override combined with a handler)"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store not found"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      }
    },
    "/api/v1/memory-rules/{memory_rule_id}": {
      "get": {
        "tags": [
          "Memory Rules"
        ],
        "summary": "Get a memory rule",
        "description": "Returns a specific memory rule",
        "operationId": "getMemoryRule",
        "x-soat-resource": {
          "kind": "memory_rule",
          "from": "memory_rule_id"
        },
        "x-iam-action": "memories:GetMemoryRule",
        "parameters": [
          {
            "name": "memory_rule_id",
            "in": "path",
            "required": true,
            "description": "Memory rule ID",
            "schema": {
              "type": "string",
              "example": "mrule_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Memory rule details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemoryRule"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory rule not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Memory Rules"
        ],
        "summary": "Update a memory rule",
        "description": "Updates a memory rule. Validation runs against the rule as it would stand after the change, so a one-field update cannot pair a handler with a stored extractor override from the side.",
        "operationId": "updateMemoryRule",
        "x-soat-resource": {
          "kind": "memory_rule",
          "from": "memory_rule_id"
        },
        "x-iam-action": "memories:UpdateMemoryRule",
        "parameters": [
          {
            "name": "memory_rule_id",
            "in": "path",
            "required": true,
            "description": "Memory rule ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "on": {
                    "$ref": "#/components/schemas/MemoryRuleEvent"
                  },
                  "source_agent_ids": {
                    "type": "array",
                    "nullable": true,
                    "description": "Send `null` to widen the rule to every agent in the project",
                    "items": {
                      "x-soat-ref": "agents",
                      "type": "string"
                    }
                  },
                  "agent_id": {
                    "x-soat-ref": "agents",
                    "type": "string",
                    "nullable": true
                  },
                  "tool_id": {
                    "x-soat-ref": "tools",
                    "type": "string",
                    "nullable": true
                  },
                  "action": {
                    "type": "string",
                    "nullable": true
                  },
                  "preset_parameters": {
                    "type": "object",
                    "nullable": true
                  },
                  "prompt": {
                    "type": "string",
                    "nullable": true
                  },
                  "ai_provider_id": {
                    "x-soat-ref": "ai-providers",
                    "type": "string",
                    "nullable": true
                  },
                  "model": {
                    "type": "string",
                    "nullable": true
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Memory rule updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemoryRule"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory rule not found"
          }
        }
      },
      "delete": {
        "tags": [
          "Memory Rules"
        ],
        "summary": "Delete a memory rule",
        "description": "Deletes a memory rule. The assertions it wrote are kept; their `rule_id` becomes null.",
        "operationId": "deleteMemoryRule",
        "x-soat-resource": {
          "kind": "memory_rule",
          "from": "memory_rule_id"
        },
        "x-iam-action": "memories:DeleteMemoryRule",
        "parameters": [
          {
            "name": "memory_rule_id",
            "in": "path",
            "required": true,
            "description": "Memory rule ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Memory rule deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory rule not found"
          }
        }
      }
    },
    "/api/v1/memory-stores": {
      "get": {
        "tags": [
          "MemoryStores"
        ],
        "summary": "List memory stores",
        "description": "Returns a list of memory store configurations for a project",
        "operationId": "listMemoryStores",
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "description": "Project ID (required if not using project key auth)",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/TagsQuery"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of memory stores",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MemoryStore"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "post": {
        "tags": [
          "MemoryStores"
        ],
        "summary": "Create a memory store",
        "description": "Creates a new memory store configuration in a project",
        "operationId": "createMemoryStore",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "additionalProperties": false,
                "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 store name",
                    "example": "Product Documentation"
                  },
                  "description": {
                    "type": "string",
                    "description": "Optional description",
                    "example": "Retrieves product docs for support queries"
                  },
                  "tags": {
                    "description": "Optional key-value tags. Scopes the memory store in knowledge search, where every requested pair must match exactly.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/TagBag"
                      }
                    ]
                  },
                  "duplicate_threshold": {
                    "type": "number",
                    "nullable": true,
                    "description": "The store's dedup policy: cosine similarity at or above which an incoming fact is already known and the write is skipped. `null` (the default) uses the algorithm constant `0.95`. A single `POST /api/v1/memories` call may override it; the agent tool and the post-turn rule never can.",
                    "minimum": 0,
                    "maximum": 1,
                    "example": 0.95
                  },
                  "supersede_threshold": {
                    "type": "number",
                    "nullable": true,
                    "description": "Cosine similarity at or above which an incoming fact restates a known one that has changed: the matched memory is invalidated and replaced. `null` (the default) uses the algorithm constant `0.90`. Must be lower than the effective `duplicate_threshold`, or the write is rejected with `400 VALIDATION_FAILED` — equal makes `superseded` unreachable and inverted swallows `skipped`.",
                    "minimum": 0,
                    "maximum": 1,
                    "example": 0.9
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Memory store created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemoryStore"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — a missing required field, a threshold outside `[0, 1]`, or a pair that is not `supersede_threshold < duplicate_threshold`."
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      }
    },
    "/api/v1/memory-stores/{memory_store_id}": {
      "get": {
        "tags": [
          "MemoryStores"
        ],
        "summary": "Get a memory store",
        "description": "Returns a single memory store configuration by ID",
        "operationId": "getMemoryStore",
        "x-soat-resource": {
          "kind": "memory_store",
          "from": "memory_store_id"
        },
        "parameters": [
          {
            "name": "memory_store_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "mstore_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Memory store found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemoryStore"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store not found"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "put": {
        "tags": [
          "MemoryStores"
        ],
        "summary": "Update a memory store",
        "description": "Updates an existing memory store configuration",
        "operationId": "updateMemoryStore",
        "x-soat-resource": {
          "kind": "memory_store",
          "from": "memory_store_id"
        },
        "parameters": [
          {
            "name": "memory_store_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "mstore_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Memory store name"
                  },
                  "description": {
                    "type": "string",
                    "nullable": true,
                    "description": "Optional description"
                  },
                  "tags": {
                    "description": "Optional key-value tags. Replaces the stored bag; `null` clears it.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/NullableTagBag"
                      }
                    ]
                  },
                  "duplicate_threshold": {
                    "type": "number",
                    "nullable": true,
                    "description": "The store's dedup policy: cosine similarity at or above which an incoming fact is already known and the write is skipped. `null` (the default) uses the algorithm constant `0.95`. A single `POST /api/v1/memories` call may override it; the agent tool and the post-turn rule never can.",
                    "minimum": 0,
                    "maximum": 1,
                    "example": 0.95
                  },
                  "supersede_threshold": {
                    "type": "number",
                    "nullable": true,
                    "description": "Cosine similarity at or above which an incoming fact restates a known one that has changed: the matched memory is invalidated and replaced. `null` (the default) uses the algorithm constant `0.90`. Must be lower than the effective `duplicate_threshold`, or the write is rejected with `400 VALIDATION_FAILED` — equal makes `superseded` unreachable and inverted swallows `skipped`.",
                    "minimum": 0,
                    "maximum": 1,
                    "example": 0.9
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Memory store updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemoryStore"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — a threshold outside `[0, 1]`, or a pair that is not `supersede_threshold < duplicate_threshold` once the stored values this request does not replace are applied."
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store not found"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "delete": {
        "tags": [
          "MemoryStores"
        ],
        "summary": "Delete a memory store",
        "description": "Deletes a memory store configuration",
        "operationId": "deleteMemoryStore",
        "x-soat-resource": {
          "kind": "memory_store",
          "from": "memory_store_id"
        },
        "parameters": [
          {
            "name": "memory_store_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "mstore_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Memory store deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store not found"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      }
    },
    "/api/v1/memory-stores/{memory_store_id}/assertions": {
      "get": {
        "tags": [
          "MemoryStores"
        ],
        "summary": "List a memory store's write assertions",
        "description": "Returns the store's write ledger, newest first: one row per write attempt, skips included. This is how \"which write path is filling this store?\" is asked — a question no column on a memory row could answer, because a skipped write produced no memory at all.",
        "operationId": "listMemoryStoreAssertions",
        "x-soat-resource": {
          "kind": "memory_store",
          "from": "memory_store_id"
        },
        "x-iam-action": "memories:ListMemoryAssertions",
        "parameters": [
          {
            "name": "memory_store_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "mstore_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "mechanism",
            "in": "query",
            "required": false,
            "description": "Only assertions that came through this door.",
            "schema": {
              "type": "string",
              "enum": [
                "tool",
                "rule",
                "api",
                "formation"
              ]
            }
          },
          {
            "name": "outcome",
            "in": "query",
            "required": false,
            "description": "Only assertions that resolved this way.",
            "schema": {
              "type": "string",
              "enum": [
                "created",
                "superseded",
                "skipped"
              ]
            }
          },
          {
            "name": "generation_id",
            "in": "query",
            "required": false,
            "description": "Only assertions made during this generation. A generation that does not exist matches nothing rather than widening to the whole store.",
            "schema": {
              "type": "string",
              "example": "gen_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Only assertions recorded at or after this instant (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Only assertions recorded at or before this instant (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of assertions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MemoryAssertion"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "An unknown `mechanism` or `outcome`, or a `from` or `to` that is not a timestamp"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store not found"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      }
    },
    "/api/v1/memory-stores/{memory_store_id}/export": {
      "get": {
        "tags": [
          "MemoryStores"
        ],
        "summary": "Export a memory store's memories as NDJSON",
        "description": "Streams one store's memories as newline-delimited JSON — one memory object per line, oldest first. Invalidated memories (retracted or superseded) are left out unless `include_invalidated` asks for them, so the file holds what the store currently asserts. The rows are the rows the listing returns for the same caller.",
        "operationId": "exportMemories",
        "x-soat-resource": {
          "kind": "memory_store",
          "from": "memory_store_id"
        },
        "x-soat-mcp-exclude": true,
        "parameters": [
          {
            "name": "memory_store_id",
            "in": "path",
            "required": true,
            "description": "Memory store whose memories are exported",
            "schema": {
              "type": "string",
              "example": "mem_store_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "include_invalidated",
            "in": "query",
            "description": "Include memories that were retracted or superseded",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "$ref": "#/components/parameters/TagsQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "A newline-delimited stream of memories. Each line is a JSON object with the same fields as `Memory`.",
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store not found"
          }
        }
      }
    },
    "/api/v1/memory-stores/{memory_store_id}/tags": {
      "get": {
        "tags": [
          "MemoryStores"
        ],
        "summary": "Get memory store tags",
        "description": "Returns all tags attached to the memory store",
        "operationId": "getMemoryStoreTags",
        "x-soat-resource": {
          "kind": "memory_store",
          "from": "memory_store_id"
        },
        "x-iam-action": "memories:GetMemoryStore",
        "parameters": [
          {
            "name": "memory_store_id",
            "in": "path",
            "required": true,
            "description": "Memory store ID",
            "schema": {
              "type": "string",
              "example": "mstore_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Memory store tags",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store not found"
          }
        }
      },
      "put": {
        "tags": [
          "MemoryStores"
        ],
        "summary": "Replace memory store tags",
        "description": "Replaces all tags on the memory store with the provided tags (not merged)",
        "operationId": "replaceMemoryStoreTags",
        "x-soat-resource": {
          "kind": "memory_store",
          "from": "memory_store_id"
        },
        "x-iam-action": "memories:UpdateMemoryStore",
        "parameters": [
          {
            "name": "memory_store_id",
            "in": "path",
            "required": true,
            "description": "Memory store ID",
            "schema": {
              "type": "string",
              "example": "mstore_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tags replaced",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "400": {
            "description": "Body is not an object of string values"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store not found"
          }
        }
      },
      "patch": {
        "tags": [
          "MemoryStores"
        ],
        "summary": "Merge memory store tags",
        "description": "Merges provided tags into the memory store's existing tags (existing tags are preserved unless overridden)",
        "operationId": "mergeMemoryStoreTags",
        "x-soat-resource": {
          "kind": "memory_store",
          "from": "memory_store_id"
        },
        "x-iam-action": "memories:UpdateMemoryStore",
        "parameters": [
          {
            "name": "memory_store_id",
            "in": "path",
            "required": true,
            "description": "Memory store ID",
            "schema": {
              "type": "string",
              "example": "mstore_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tags merged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "400": {
            "description": "Body is not an object of string values"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store not found"
          }
        }
      }
    },
    "/api/v1/metadata-schemas": {
      "get": {
        "tags": [
          "Metadata Schemas"
        ],
        "summary": "List metadata schemas",
        "description": "Returns the declarations in scope, oldest first. `resource_type` narrows them to one governed resource.",
        "operationId": "listMetadataSchemas",
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "description": "Project public ID (proj_ prefix)",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "resource_type",
            "in": "query",
            "required": false,
            "description": "Return only the declarations governing this resource.",
            "schema": {
              "type": "string",
              "enum": [
                "document"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "The declarations in scope",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MetadataSchemaRecord"
                      }
                    },
                    "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": [
          "Metadata Schemas"
        ],
        "summary": "Declare a metadata schema",
        "description": "Declares what `metadata` must satisfy for one resource type under one selector. A document is selected by `path_prefix`, matched on a path boundary: `/reports` covers `/reports/q1.txt` and never `/reports-archive/q1.txt`.\n\nThe schema is compiled here, so one JSON Schema cannot parse is refused rather than stored — a stored one would be a rule that silently governs nothing. One selector has one schema per resource type; a second declaration of the same one is `409 NAME_CONFLICT`. The reserved root `/.system` cannot be governed: a platform-written document carries no caller metadata.\n",
        "operationId": "createMetadataSchema",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "resource_type",
                  "schema"
                ],
                "properties": {
                  "project_id": {
                    "x-soat-ref": "projects",
                    "type": "string",
                    "description": "Project the declaration governs. Optional for a project-scoped API key, which supplies it.",
                    "example": "proj_V1StGXR8Z5jdHi6B"
                  },
                  "resource_type": {
                    "type": "string",
                    "enum": [
                      "document"
                    ],
                    "description": "The resource whose metadata this declaration governs. A type appears here once its write path reads the registry, so a declaration always has a door that enforces it.",
                    "example": "document"
                  },
                  "path_prefix": {
                    "type": "string",
                    "description": "The selector a `document` declaration must carry: the directory it governs.",
                    "example": "/reports"
                  },
                  "schema": {
                    "type": "object",
                    "description": "A JSON Schema. Its keywords are its own vocabulary and are stored as written.",
                    "example": {
                      "type": "object",
                      "required": [
                        "quarter"
                      ],
                      "properties": {
                        "quarter": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The declaration",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MetadataSchemaRecord"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request — unknown `resource_type`, a missing or unusable selector, the reserved root, or a schema that is not valid JSON Schema",
            "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": "Conflict — this selector is already declared for this type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/metadata-schemas/validate": {
      "post": {
        "tags": [
          "Metadata Schemas"
        ],
        "summary": "Check metadata against what is declared",
        "description": "Answers what a write would be told, without writing: a caller preparing a batch learns which declaration would refuse it, and why, before it sends anything.\n\nIt reports; it does not enforce. The refusal itself lives in each resource's own write path, because a check a writer has to call is advisory and the writer who skips it is the one the rule exists for.\n",
        "operationId": "validateMetadata",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "path"
                ],
                "properties": {
                  "project_id": {
                    "x-soat-ref": "projects",
                    "type": "string",
                    "example": "proj_V1StGXR8Z5jdHi6B"
                  },
                  "path": {
                    "type": "string",
                    "description": "The path the document would be filed at, which decides which declaration governs it.",
                    "example": "/reports/q1.txt"
                  },
                  "metadata": {
                    "description": "The bag to judge. Absent or `null` is judged as an empty bag.",
                    "example": {
                      "quarter": "Q1"
                    },
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/NullableMetadataBag"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The verdict",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "valid",
                    "resource_type",
                    "metadata_schema_id",
                    "path_prefix",
                    "error"
                  ],
                  "properties": {
                    "valid": {
                      "type": "boolean",
                      "description": "Whether a write of this metadata at this path would be accepted.",
                      "example": false
                    },
                    "resource_type": {
                      "type": "string",
                      "enum": [
                        "document"
                      ],
                      "example": "document"
                    },
                    "metadata_schema_id": {
                      "type": "string",
                      "nullable": true,
                      "description": "The declaration that would refuse it; `null` when the metadata is accepted or nothing governs the path.",
                      "example": "mdschema_V1StGXR8Z5jdHi6B"
                    },
                    "path_prefix": {
                      "type": "string",
                      "nullable": true,
                      "description": "The refusing declaration's selector.",
                      "example": "/reports"
                    },
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/StoredError"
                        }
                      ],
                      "nullable": true,
                      "description": "The error the write would be refused with; its message names what the metadata violates, field by field."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/metadata-schemas/{metadata_schema_id}": {
      "get": {
        "tags": [
          "Metadata Schemas"
        ],
        "summary": "Get a metadata schema",
        "description": "Returns one declaration by id.",
        "operationId": "getMetadataSchema",
        "x-soat-resource": {
          "kind": "metadata_schema",
          "from": "metadata_schema_id"
        },
        "x-iam-action": "metadata-schemas:GetMetadataSchema",
        "parameters": [
          {
            "name": "metadata_schema_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "mdschema_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The declaration",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MetadataSchemaRecord"
                }
              }
            }
          },
          "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": [
          "Metadata Schemas"
        ],
        "summary": "Update a metadata schema",
        "description": "Changes the schema, the selector, or both. `resource_type` is fixed at creation: it decides the selector's spelling and which write path reads the row, so changing it would silently repoint the declaration at a different door — delete it and declare again instead.",
        "operationId": "updateMetadataSchema",
        "x-soat-resource": {
          "kind": "metadata_schema",
          "from": "metadata_schema_id"
        },
        "x-iam-action": "metadata-schemas:UpdateMetadataSchema",
        "parameters": [
          {
            "name": "metadata_schema_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "mdschema_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "path_prefix": {
                    "type": "string",
                    "description": "The declaration's new selector.",
                    "example": "/reports/quarterly"
                  },
                  "schema": {
                    "type": "object",
                    "description": "Replaces the declared JSON Schema.",
                    "example": {
                      "type": "object",
                      "required": [
                        "quarter",
                        "owner"
                      ],
                      "properties": {
                        "quarter": {
                          "type": "string"
                        },
                        "owner": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The declaration",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MetadataSchemaRecord"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request — an unusable selector, or a schema that is not valid JSON Schema",
            "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"
                }
              }
            }
          },
          "409": {
            "description": "Conflict — this selector is already declared for this type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Metadata Schemas"
        ],
        "summary": "Delete a metadata schema",
        "description": "Removes the declaration. Documents already stored keep the metadata they hold — the rule governed writes, not rows.",
        "operationId": "deleteMetadataSchema",
        "x-soat-resource": {
          "kind": "metadata_schema",
          "from": "metadata_schema_id"
        },
        "x-iam-action": "metadata-schemas:DeleteMetadataSchema",
        "parameters": [
          {
            "name": "metadata_schema_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "mdschema_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "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/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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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"
                ],
                "additionalProperties": false,
                "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",
        "x-soat-resource": {
          "kind": "model_route",
          "from": "route_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "model_route",
          "from": "route_id"
        },
        "parameters": [
          {
            "name": "route_id",
            "in": "path",
            "required": true,
            "description": "Model route ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "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",
        "x-soat-resource": {
          "kind": "model_route",
          "from": "route_id"
        },
        "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\ndocument: where `/authorize`, `/token` and `/register` are, which grants and PKCE methods are\nsupported, and which scopes exist.\n\nDiscovery precedes authentication by definition, so this endpoint takes no credential.\n",
        "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\ndocument, naming the authorization server that guards `/mcp`. An unauthenticated request to\n`/mcp` answers `401` with a `WWW-Authenticate: Bearer resource_metadata=\"…\"` header pointing\nhere, which is how an OAuth-aware MCP client bootstraps the flow.\n",
        "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\nself-serve half of the flow, which lets a client onboard itself with no operator step.\n\nA client that registers with `token_endpoint_auth_method: none` is public and receives no\nsecret; any other method yields a `client_secret` that does not expire.\n",
        "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\n`code_challenge_method` must be `S256`; OAuth 2.1 forbids `plain`.\n\nA user who has not yet consented is redirected to the consent screen at\n`/app/oauth/consent`; once consent is recorded, a second call to this endpoint redirects to\n`redirect_uri` with a single-use `code`. Parameter errors that can be attributed to a\nregistered `redirect_uri` are reported as a redirect carrying `error`, per RFC 6749;\nan unknown `client_id` or an unregistered `redirect_uri` answers `400` directly, because\nthere is no trustworthy place to send the user.\n",
        "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\nstill needed, or back to `redirect_uri` with `error` for a parameter the client can fix.\n",
            "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\nauthenticates with `client_secret_basic`, `client_secret_post`, or `none` for a public client.\n\nThe access token it returns is a SOAT-issued JWT: send it as `Authorization: Bearer <token>`\nto `/mcp` and to the REST API. Refresh tokens are single-use and rotated on every exchange.\n",
        "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"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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 `problems` and non-blocking `warnings` (e.g. a state key only written on a conditional branch). The same checks are enforced on create and update, which fail with `400 ORCHESTRATION_VALIDATION_FAILED` listing the same items under `meta.problems` and `meta.warnings`.\n",
        "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/OrchestrationValidationResult"
                }
              }
            }
          },
          "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. The per-project breakdown is listed by `GET /api/v1/orchestrations/queue/stats/projects`. Intended for admin/operator policies; guarded by `orchestrations:GetQueueStats`.\nEvery figure is scoped to what the caller may see. A project-scoped caller gets `queue_depth` and `claimed_tasks` summed over their own projects, and `null` for `oldest_queued_age_seconds` and the `claim_latency_ms` percentiles — both describe the whole deployment and cannot be narrowed, so they are withheld rather than approximated. An unrestricted caller (the action granted on every project) gets the deployment-wide figures.\n",
        "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/queue/stats/projects": {
      "get": {
        "tags": [
          "Orchestrations"
        ],
        "summary": "List the projects with queued orchestration work",
        "description": "One row per project with any queued or claimed task, ordered by project ID: the per-project breakdown of `GET /api/v1/orchestrations/queue/stats`. A project-scoped caller sees its own projects only. Guarded by `orchestrations:GetQueueStats`.\n",
        "operationId": "listQueueStatsProjects",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of projects with queued or claimed work",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/QueueProjectStats"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "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",
        "x-soat-resource": {
          "kind": "orchestration",
          "from": "orchestration_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "orchestration",
          "from": "orchestration_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/orchestration_id"
          },
          {
            "$ref": "#/components/parameters/IfMatchVersion"
          }
        ],
        "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"
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      },
      "delete": {
        "tags": [
          "Orchestrations"
        ],
        "summary": "Delete an orchestration",
        "description": "Deletes an orchestration definition and all its runs.",
        "operationId": "deleteOrchestration",
        "x-soat-resource": {
          "kind": "orchestration",
          "from": "orchestration_id"
        },
        "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).\n",
        "operationId": "listOrchestrationVersions",
        "x-soat-resource": {
          "kind": "orchestration",
          "from": "orchestration_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/orchestration_id"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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.\n",
        "operationId": "getOrchestrationVersion",
        "x-soat-resource": {
          "kind": "orchestration",
          "from": "orchestration_id"
        },
        "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.\n\nThe 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.\n",
        "operationId": "restoreOrchestrationVersion",
        "x-soat-resource": {
          "kind": "orchestration",
          "from": "orchestration_id"
        },
        "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"
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      }
    },
    "/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/completed/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",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StartOrchestrationRunRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Duplicate request — the run the `idempotency_key` already names is returned and no second run is started.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrchestrationRun"
                }
              }
            }
          },
          "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, `metadata` that is not a JSON object, or an `idempotency_key` that is not a non-empty string of at most 255 characters). No run is created."
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Orchestration not found"
          },
          "409": {
            "description": "`IDEMPOTENCY_KEY_REUSED` — the key is already claimed by a run started from a different request. No run is created."
          }
        }
      },
      "get": {
        "tags": [
          "Orchestrations"
        ],
        "summary": "List orchestration runs",
        "description": "Returns orchestration runs the caller can access, optionally filtered by orchestration, by parent run, by status, or by whether the run has a parent at all.\n\nNote 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": "project_id",
            "in": "query",
            "required": false,
            "description": "Narrow the listing to one project (proj_...). A project-scoped key lists its own project without it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "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.\n\nContradicting `parent_orchestration_run_id` with `nested=false` is a `400`; any value other than `true` or `false` is a `400`.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by run status. Repeat the parameter to OR values — `status=queued&status=running&status=sleeping&status=awaiting_input` is the set still driving, which is how a caller finds live work without paging every run the project ever started.\n\nThere is no `non_terminal` shorthand on purpose: which statuses count as live is the caller's policy. A value outside the enum, empty string included, is a `400`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "queued",
                  "running",
                  "sleeping",
                  "awaiting_input",
                  "completed",
                  "failed",
                  "cancelled",
                  "expired"
                ]
              }
            },
            "style": "form",
            "explode": true,
            "example": [
              "queued",
              "running"
            ]
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
        "x-soat-resource": {
          "kind": "orchestration_run",
          "from": "orchestration_run_id"
        },
        "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}/pause": {
      "post": {
        "tags": [
          "Orchestrations"
        ],
        "summary": "Pause an orchestration run",
        "description": "Parks a run in flight as `awaiting_input` at its next checkpoint, with a `required_action` of type `paused` naming the pause as operator-initiated rather than a node's. Unlike cancel, the run keeps its last checkpoint and resume-orchestration-run re-drives it from there — so work already done is deferred rather than discarded.\nA `queued` or `sleeping` run is parked immediately (a sleeping run keeps the wake it was due, and resuming hands it back to the scheduler at that instant). A `running` run keeps running until the round in flight reaches its checkpoint, so the response may still read `running` while `pause_requested_at` is set. A run already parked on a human, webhook or approval node keeps that node's `required_action`; the pause is still recorded, which is what makes submit-human-input refuse until the run is resumed.\nThe pause fans out to the run's `loop` / `sub_orchestration` descendants — each parks at its own next checkpoint — because otherwise a parent's pause would bound nothing. Resuming does not fan out: each parked descendant is resumed by its own id.\nIdempotent: pausing an already-paused run answers with it unchanged.",
        "operationId": "pauseOrchestrationRun",
        "x-soat-resource": {
          "kind": "orchestration_run",
          "from": "orchestration_run_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/orchestration_run_id"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PauseOrchestrationRunRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Paused run",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrchestrationRun"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          },
          "409": {
            "description": "Run has already settled"
          }
        }
      }
    },
    "/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",
        "x-soat-resource": {
          "kind": "orchestration_run",
          "from": "orchestration_run_id"
        },
        "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, or an operator pause is in force"
          }
        }
      }
    },
    "/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.\nIt is also the only thing that lifts an operator pause (pause-orchestration-run): a run parked with `required_action.type` of `paused` re-drives the frontier that had not run yet, and one paused mid-timer goes back to `sleeping` for the wake it was already due.",
        "operationId": "resumeOrchestrationRun",
        "x-soat-resource": {
          "kind": "orchestration_run",
          "from": "orchestration_run_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "orchestration_run",
          "from": "orchestration_run_id"
        },
        "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/orchestration-runs/{orchestration_run_id}/node-executions": {
      "get": {
        "tags": [
          "Orchestrations"
        ],
        "summary": "List a run's node executions",
        "description": "The run's per-node execution records, oldest first: one per node attempt, retries and skipped nodes included. Each captures the resolved input, output, status and error of that attempt — the orchestration analogue of an LLM trace. Reading them is reading the run (`orchestrations:GetRun`).",
        "operationId": "listOrchestrationRunNodeExecutions",
        "x-soat-resource": {
          "kind": "orchestration_run",
          "from": "orchestration_run_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/orchestration_run_id"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the run's node executions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NodeExecution"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/playbooks": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "List playbooks",
        "description": "Returns the playbooks defined in a project, newest first.",
        "operationId": "listPlaybooks",
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "description": "Project ID (required if not using project key auth)",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of playbooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Playbook"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Create a playbook",
        "description": "Creates a playbook at version 1. The definition is refused with `400 PLAYBOOK_VALIDATION_FAILED` when it holds a duplicate node or input, an input reading a missing node or field, an `execute` node without an executor or another kind with one, transformations on a non-final node, or a cycle; `meta.problems` lists every one. A duplicate `name` in the project is `409 NAME_CONFLICT`.\n\nThe caller becomes the version's author, whom its executors run as, so it must be allowed to start every executor the definition names (`403 PLAYBOOK_EXECUTOR_FORBIDDEN`). A definition with executors written with a trigger or OAuth token is `409 PLAYBOOK_AUTHOR_UNAVAILABLE`: those carry their boundary in the token, so no run can act as them.",
        "operationId": "createPlaybook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePlaybookRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Playbook created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Playbook"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — a malformed or invalid definition"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "409": {
            "description": "A playbook with this name already exists in the project"
          }
        }
      }
    },
    "/api/v1/playbooks/validate": {
      "post": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Validate a playbook definition",
        "description": "Answers what a create or update would be told about a definition, without writing: `valid`, and every graph problem it holds. A definition whose shape cannot be read is `400 VALIDATION_FAILED`, as on a write.",
        "operationId": "validatePlaybook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ValidatePlaybookRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookValidationResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — the definition's shape cannot be read"
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/api/v1/playbooks/{playbook_id}": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Get a playbook",
        "description": "Returns a playbook with its current definition and version.",
        "operationId": "getPlaybook",
        "x-soat-resource": {
          "kind": "playbook",
          "from": "playbook_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/PlaybookId"
          }
        ],
        "responses": {
          "200": {
            "description": "The playbook",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Playbook"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Playbook not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Update a playbook",
        "description": "Updates any of `name`, `description` and `definition`. Only a change to `definition` archives a new version; a rename or a rewrite of the definition the playbook already holds leaves `version` where it is. A case keeps the version it was created on.\n\nA new version is authored by the caller, checked as on create: `403 PLAYBOOK_EXECUTOR_FORBIDDEN` for an executor it may not start, `409 PLAYBOOK_AUTHOR_UNAVAILABLE` for a trigger or OAuth token.",
        "operationId": "updatePlaybook",
        "x-soat-resource": {
          "kind": "playbook",
          "from": "playbook_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/PlaybookId"
          },
          {
            "$ref": "#/components/parameters/IfMatchVersion"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdatePlaybookRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Playbook updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Playbook"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — a malformed or invalid definition"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Playbook not found"
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      },
      "delete": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Delete a playbook",
        "description": "Deletes a playbook and its version archive. A playbook with cases is refused with `409 PLAYBOOK_HAS_CASES`.",
        "operationId": "deletePlaybook",
        "x-soat-resource": {
          "kind": "playbook",
          "from": "playbook_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/PlaybookId"
          }
        ],
        "responses": {
          "204": {
            "description": "Playbook deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Playbook not found"
          },
          "409": {
            "description": "The playbook has cases"
          }
        }
      }
    },
    "/api/v1/playbooks/{playbook_id}/versions": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "List a playbook's versions",
        "description": "Returns the playbook's archived definitions, newest first. A version is written on create and on every write that changes `definition`.",
        "operationId": "listPlaybookVersions",
        "x-soat-resource": {
          "kind": "playbook",
          "from": "playbook_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/PlaybookId"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of playbook versions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlaybookVersion"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Playbook not found"
          }
        }
      }
    },
    "/api/v1/playbooks/{playbook_id}/versions/{version}": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Fetch an archived playbook version",
        "description": "Returns the definition a version held: what a case pinned to it is evaluated against.",
        "operationId": "getPlaybookVersion",
        "x-soat-resource": {
          "kind": "playbook",
          "from": "playbook_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/PlaybookId"
          },
          {
            "$ref": "#/components/parameters/Version"
          }
        ],
        "responses": {
          "200": {
            "description": "Archived playbook version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookVersion"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — version is not a positive integer"
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/playbooks/{playbook_id}/versions/{version}/restore": {
      "post": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Restore an archived playbook version",
        "description": "Writes an archived definition back as the playbook's live one, which archives it again as a new version rather than rewinding the counter. Restoring the definition the playbook already holds is a no-op. The caller authors the new version, checked as on create.",
        "operationId": "restorePlaybookVersion",
        "x-soat-resource": {
          "kind": "playbook",
          "from": "playbook_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/PlaybookId"
          },
          {
            "$ref": "#/components/parameters/Version"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RestorePlaybookVersionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The playbook, at its new version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Playbook"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — version is not a positive integer"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      }
    },
    "/api/v1/playbooks/{playbook_id}/cases": {
      "post": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Create a case",
        "description": "Creates a case for one subject on the playbook's current version. The case stays on that version however the playbook changes later.",
        "operationId": "createPlaybookCase",
        "x-soat-resource": {
          "kind": "playbook",
          "from": "playbook_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/PlaybookId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePlaybookCaseRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Case created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookCase"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — subject is missing"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Playbook not found"
          }
        }
      }
    },
    "/api/v1/playbook-cases": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "List playbook cases",
        "description": "Returns a project's cases, newest first, without node states; read one case for those.",
        "operationId": "listPlaybookCases",
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "description": "Project ID (required if not using project key auth)",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "playbook_id",
            "in": "query",
            "description": "Only the cases of this playbook",
            "schema": {
              "type": "string",
              "example": "pbk_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "automation_status",
            "in": "query",
            "required": false,
            "description": "Only the cases in these automation statuses. Repeat the parameter to OR values. A value outside the enum, empty string included, is a `400`.",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/PlaybookAutomationStatus"
              }
            },
            "style": "form",
            "explode": true
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of cases",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlaybookCaseSummary"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Get a case",
        "description": "Returns a case with every node's state, computed from its entries on each read and never stored.",
        "operationId": "getPlaybookCase",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          }
        ],
        "responses": {
          "200": {
            "description": "The case",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookCase"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Case not found"
          }
        }
      },
      "delete": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Delete a case",
        "description": "Deletes a case and every entry written to it.",
        "operationId": "deletePlaybookCase",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          }
        ],
        "responses": {
          "204": {
            "description": "Case deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Case not found"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/inputs": {
      "post": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Write a case's inputs",
        "description": "Writes a batch of manual inputs, all or none. Each write names the revision it read in `expected_revision`, `null` for a field still unset; when any field has moved on the whole batch is refused with `409 PLAYBOOK_INPUT_CONFLICT` and `meta.conflicts` names each field. An input the case's version does not declare, one that reads another node, or a value its schema refuses is `400 PLAYBOOK_INPUT_INVALID`. A value equal to the field's current one writes no revision. Once written, every node the write makes runnable is queued to run, unless the case is paused. With `wait: true` it answers once every execution queued, and every one those queue, has settled or parked. A write made with a credential the case's own work runs as — the case, one of its executions in flight, or the task or run one started — is `403 PLAYBOOK_SELF_WRITE`.",
        "operationId": "writePlaybookCaseInputs",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WritePlaybookCaseInputsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The case after the write",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookCase"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — an unknown input, a non-manual input or a value its schema refuses"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Case not found"
          },
          "409": {
            "description": "A field changed since it was read; nothing was written"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/missing": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "List what a node still needs",
        "description": "Returns the manual inputs still unset, or refused by their schema, upstream of `node_id`, grouped in waves: nothing in a later wave can run before the earlier ones' required inputs are filled. Optional inputs are listed with `required: false`; they never block a node.",
        "operationId": "getPlaybookCaseMissing",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          },
          {
            "name": "node_id",
            "in": "query",
            "required": true,
            "description": "The ID of the node to reach, usually a final node",
            "schema": {
              "type": "string",
              "example": "pricing"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The missing inputs, by wave",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookCaseMissing"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — node_id names no node of the case's version"
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Case not found"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/entries": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "List a case's entries",
        "description": "Returns every value written to the case, newest first. Entries are never rewritten, so this is the case's full history.",
        "operationId": "listPlaybookCaseEntries",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          },
          {
            "name": "node_id",
            "in": "query",
            "description": "Only the entries of this node",
            "schema": {
              "type": "string",
              "example": "revenue"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of entries, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlaybookEntry"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Case not found"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/nodes/{node}/run": {
      "post": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Run a node of a case",
        "description": "Runs one node now. The cascade already runs every node whose inputs are set once a write, a settle or a resume makes it runnable, and never reruns a `failed` or `done` one: this is how a failed node is retried, and how a `done` node is recomputed after an upgrade changed its executor. A `collect` node takes its own input values as its output, any other node starts its executor with the node's input values mapped through the executor's `input`. The executor runs as the author of the case's playbook version, whose permission to start it is checked again here (`403 PLAYBOOK_EXECUTOR_FORBIDDEN`); an author that no longer exists is `409 PLAYBOOK_AUTHOR_UNAVAILABLE`. Only a `ready`, `stale`, `failed` or `done` node with every required input set runs; anything else is `409 PLAYBOOK_NODE_NOT_RUNNABLE`. A paused case is `409 PLAYBOOK_CASE_PAUSED`, and a case in a paused project `409 PROJECT_PAUSED`.\n\nThe output is written as an entry when the execution settles. A value equal to the node's current output writes no revision. If the node's inputs change while it runs, the execution settles as `superseded` and writes nothing.\n\nWithout `wait` the execution answers `running` and settles in the background. With `wait: true` it answers once the executor settles, or once it parks: a workflow task still open, or an orchestration run waiting on a person or a timer, settles the node when it closes or ends.",
        "operationId": "runPlaybookCaseNode",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          },
          {
            "$ref": "#/components/parameters/NodeKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RunPlaybookCaseNodeRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The execution, `running` or settled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookExecution"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — a node the case's version does not declare"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden, or the author may not start the executor"
          },
          "404": {
            "description": "Case not found"
          },
          "409": {
            "description": "The node cannot run now, the case is paused, or the version has no author to run as"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/executions": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "List a case's executions",
        "description": "Returns every run of the case's nodes, newest first.",
        "operationId": "listPlaybookCaseExecutions",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          },
          {
            "name": "node_id",
            "in": "query",
            "description": "Only the executions of this node",
            "schema": {
              "type": "string",
              "example": "revenue"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of executions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlaybookExecution"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Case not found"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/executions/{execution_id}": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Get an execution",
        "description": "Returns one run of a case's node.",
        "operationId": "getPlaybookCaseExecution",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          },
          {
            "$ref": "#/components/parameters/ExecutionId"
          }
        ],
        "responses": {
          "200": {
            "description": "The execution",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookExecution"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Case or execution not found"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/nodes/{node}/exports": {
      "post": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Export a final node",
        "description": "Renders one revision of a final node as files, one export per transformation: each transformation's tool is called with the case's `case_id` and `subject`, the `node_id`, the `revision` and that revision's `output`, and must answer a file (`execute.response_mode: base64`) of its `format`'s content type. The file is kept in the project's files under `/.system/playbooks/`. An export never feeds a node.\n\nWithout `revision` the node's current output is exported, which needs the node `done` (`409 PLAYBOOK_NODE_NOT_EXPORTABLE`); a `revision` names an earlier output or confirmation of the node. Each tool runs as the author of the case's playbook version, whose permission to call it is checked here (`403 PLAYBOOK_EXECUTOR_FORBIDDEN`). A paused case is `409 PLAYBOOK_CASE_PAUSED`, and a case in a paused project `409 PROJECT_PAUSED`.\n\nWithout `wait` every export answers `running` and settles in the background; with `wait: true` it answers once each has its file or its error.",
        "operationId": "exportPlaybookCaseNode",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          },
          {
            "$ref": "#/components/parameters/NodeKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportPlaybookCaseNodeRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "One export per transformation, `running` or settled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlaybookExport"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — a node the version does not declare or that has no transformations, a transformation it does not declare, or a revision that is no output of the node"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden, or the author may not call a transformation's tool"
          },
          "404": {
            "description": "Case not found"
          },
          "409": {
            "description": "The node has no current output, the case is paused, or the version has no author to run as"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/exports": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "List a case's exports",
        "description": "Returns every file rendered from the case's final nodes, newest first.",
        "operationId": "listPlaybookCaseExports",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          },
          {
            "name": "node_id",
            "in": "query",
            "description": "Only the exports of this node",
            "schema": {
              "type": "string",
              "example": "pricing"
            }
          },
          {
            "name": "transformation",
            "in": "query",
            "description": "Only the exports of this transformation",
            "schema": {
              "type": "string",
              "example": "proposal"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of exports, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlaybookExport"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Case not found"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/exports/{export_id}": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Get an export",
        "description": "Returns one file rendered from a case's final node.",
        "operationId": "getPlaybookCaseExport",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          },
          {
            "$ref": "#/components/parameters/ExportId"
          }
        ],
        "responses": {
          "200": {
            "description": "The export",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookExport"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Case or export not found"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/upgrade": {
      "post": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Upgrade a case",
        "description": "Moves a case to another version of its playbook: the playbook's current version, or the `version` named, an earlier one included. Values carry over by node ID, and every node's state is read against the new version: a node added is open, a node removed leaves the structure while its entries stay, a node whose input reads another field is `stale`, and a value its input's new schema refuses is missing again. The cascade then runs what became runnable. With `wait: true` it answers once every execution queued, and every one those queue, has settled or parked. Each upgrade is recorded with what moved; the version the case is already on records nothing.\n\nAn execution running is `409 PLAYBOOK_CASE_BUSY`; one queued but not started settles `superseded`. An upgrade made with a credential the case's own work runs as is `403 PLAYBOOK_SELF_WRITE`.",
        "operationId": "upgradePlaybookCase",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpgradePlaybookCaseRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The case on its new version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookCase"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — a version the playbook does not have"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden, or an upgrade from the case's own work"
          },
          "404": {
            "description": "Case not found"
          },
          "409": {
            "description": "An execution of the case is running"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/upgrades": {
      "get": {
        "tags": [
          "Playbooks"
        ],
        "summary": "List a case's upgrades",
        "description": "Returns every version change of the case, newest first.",
        "operationId": "listPlaybookCaseUpgrades",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of upgrades, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlaybookUpgrade"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Case not found"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/pause": {
      "post": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Pause a case",
        "description": "Stops the case starting anything: no node is queued and no queued execution starts until it is resumed. Executions already running settle. Input writes are still accepted. Pausing a paused case keeps the first pause.",
        "operationId": "pausePlaybookCase",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          }
        ],
        "responses": {
          "200": {
            "description": "The paused case",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookCase"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Case not found"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/resume": {
      "post": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Resume a case",
        "description": "Lifts the pause and starts what became runnable while the case was paused. A case that is not paused is `409 PLAYBOOK_CASE_NOT_PAUSED`; one in a paused project is `409 PROJECT_PAUSED` and stays paused until the project resumes.",
        "operationId": "resumePlaybookCase",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          }
        ],
        "responses": {
          "200": {
            "description": "The resumed case",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookCase"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Case not found"
          },
          "409": {
            "description": "The case is not paused"
          }
        }
      }
    },
    "/api/v1/playbook-cases/{case_id}/nodes/{node}/confirmation": {
      "post": {
        "tags": [
          "Playbooks"
        ],
        "summary": "Confirm a node of a case",
        "description": "Approves or rejects a `confirm` node on the inputs the confirmer read. `expected_revisions` names the revision of each of the node's inputs as read (`nodes[].inputs[].revision` on the case); an input that moved since is `409 PLAYBOOK_INPUT_CONFLICT`, `meta.conflicts` naming each, and nothing is written.\n\nAn approval writes a `confirmation` entry whose value is the node's input values: it is the node's output, and the nodes reading it run next. With `wait: true` it answers once every execution queued, and every one those queue, has settled or parked. A rejection writes a `rejection` entry with the `reason`: the node is `rejected`, and every node reading it is blocked, until its inputs change or it is approved. Another node, or a `confirm` node with a required input unset or reading a node that is not `done`, is `409 PLAYBOOK_NODE_NOT_CONFIRMABLE`. A confirmation made with a credential the case's own work runs as is `403 PLAYBOOK_SELF_WRITE`.",
        "operationId": "confirmPlaybookCaseNode",
        "x-soat-resource": {
          "kind": "playbook_case",
          "from": "case_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CaseId"
          },
          {
            "$ref": "#/components/parameters/NodeKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfirmPlaybookCaseNodeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The case after the confirmation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybookCase"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — an unknown decision, a rejection without a reason, or a node the version does not declare"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden, or a write from the case's own work"
          },
          "404": {
            "description": "Case not found"
          },
          "409": {
            "description": "The node cannot be confirmed now, or an input changed since it was read"
          }
        }
      }
    },
    "/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.\n",
        "operationId": "listPolicies",
        "parameters": [
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "description": "Return only policies attached to this user (user_...)",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
        "x-soat-agent-exclude": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "document"
                ],
                "additionalProperties": false,
                "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",
        "x-soat-agent-exclude": true,
        "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"
                ],
                "additionalProperties": false,
                "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",
        "x-soat-agent-exclude": true,
        "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.\n",
        "operationId": "listProjects",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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"
                ],
                "additionalProperties": false,
                "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_orchestration_run_depth`), its inherited model route (`default_model_route_id`), its read-auditing opt-in (`audit_reads_enabled`), its priced-model requirement (`require_priced_model`), 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",
                "additionalProperties": false,
                "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_orchestration_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`."
                  },
                  "require_priced_model": {
                    "type": "boolean",
                    "example": true,
                    "description": "Refuse a generation whose model carries no price row for every billable token component, with `409 MODEL_NOT_PRICED`. Defaults to `false`, where an unpriced model runs and meters at `cost_usd: null`. The check runs before the provider is called, so a refused turn spends nothing, and covers every target of a model route the turn may fail over to."
                  },
                  "default_conversation_retrieval": {
                    "type": "string",
                    "enum": [
                      "embed",
                      "none"
                    ],
                    "example": "none",
                    "description": "What a conversation that names no `retrieval` of its own does. `none` by default: a turn is embedded because someone asked for the conversation to be retrievable, never merely because it was said. Turns are stored and chunked on both settings."
                  },
                  "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.\n\nEvery 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, memory stores, 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.\n",
        "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`.\n",
            "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}/pause": {
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Pause a project",
        "description": "Stops everything the project runs, with one call. From the moment it answers, nothing new starts in the project: agent, session and conversation generations, tool-output continuations, chat completions, direct tool calls, orchestration runs, eval runs and trigger firings are refused with `409 PROJECT_PAUSED`, and so is resuming a single run or task the pause holds.\nWhat is already in motion is parked rather than discarded: every live orchestration run is paused the way pause-orchestration-run pauses it (a running one parks at its next checkpoint), every open task's automation is paused, schedule triggers stop firing, and queued eval items wait unclaimed. A generation or node already in flight finishes. Reads and configuration writes keep working.\nIdempotent: pausing a paused project answers it unchanged and keeps the first `reason`. Emits `projects.paused`.",
        "operationId": "pauseProject",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project public ID (proj_ prefix)",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PauseProjectRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The paused project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectRecord"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request (reason is not a string of at most 256 characters)"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Project not found"
          }
        }
      }
    },
    "/api/v1/projects/{project_id}/resume": {
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Resume a project",
        "description": "Lifts the project's pause and hands back exactly what it held: the runs and tasks the pause parked are resumed (a run the way resume-orchestration-run resumes it, in the background; a task's suppressed dispatch runs as the caller), and schedule triggers fire again from their next occurrence after now — an occurrence that fell due while paused is not fired late. A run or task an operator paused before the project was paused keeps its own pause. Eval runs continue from the item they stopped at. Emits `projects.resumed`.",
        "operationId": "resumeProject",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project public ID (proj_ prefix)",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resumed project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectRecord"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Project not found"
          },
          "409": {
            "description": "The project is not paused",
            "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.\n",
        "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.\n",
        "operationId": "updateProjectPrices",
        "x-soat-agent-exclude": true,
        "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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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`; `storage_bytes` is valid for `scope: project` only. 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 `cost_usd` quota may name one `meter_type` to cap; omitting it caps every priced meter. A duplicate quota (same project, scope, scope_ref, metric, window, meter_type) is rejected with 409.\n\n`storage_bytes` caps a stored total rather than a windowed one, so it takes `window: current` and every other metric refuses that value (400 either way). It is enforced at the corpus write paths — file upload and create, document create, document ingest and re-ingest, memory create — with `409 QUOTA_STORAGE_EXCEEDED` and no `Retry-After`, since no window reset clears a footprint.",
        "operationId": "createQuota",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "scope",
                  "metric",
                  "window",
                  "limit"
                ],
                "additionalProperties": false,
                "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",
                      "storage_bytes"
                    ],
                    "description": "The metric being capped",
                    "example": "requests"
                  },
                  "window": {
                    "type": "string",
                    "enum": [
                      "rolling_1m",
                      "rolling_1h",
                      "rolling_24h",
                      "calendar_month",
                      "current"
                    ],
                    "description": "The window over which the metric is aggregated. `current` is the only accepted value for storage_bytes (a stored total is not aggregated over time) and is refused for every other metric."
                  },
                  "limit": {
                    "type": "number",
                    "description": "The cap. Must be a positive integer for requests/tokens/storage_bytes (bytes); 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 quotas.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. A partly priced window is not a blackout: no posture refuses it, it is enforced on its priced total, and it files the same exception.",
                    "example": "block"
                  },
                  "meter_type": {
                    "type": "string",
                    "enum": [
                      "llm_tokens",
                      "compute_execution",
                      "api_request",
                      "storage",
                      "tool_execution"
                    ],
                    "description": "Only for metric cost_usd (400 on any other metric). The meter this cap answers for. Omit it and the cap sums every priced meter, which is the existing behaviour; name one and only that meter's cost counts, so an AI spend cap is not consumed by platform meters the operator prices (and vice versa). Part of the quota's identity, so two meter scopes can share a scope/metric/window and neither conflicts with an unscoped cap. Immutable after creation — replace the quota to change it."
                  }
                }
              }
            }
          }
        },
        "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",
        "x-soat-resource": {
          "kind": "quota",
          "from": "quota_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "quota",
          "from": "quota_id"
        },
        "parameters": [
          {
            "name": "quota_id",
            "in": "path",
            "required": true,
            "description": "Quota ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "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",
        "x-soat-resource": {
          "kind": "quota",
          "from": "quota_id"
        },
        "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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
        "x-soat-agent-exclude": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "value"
                ],
                "additionalProperties": false,
                "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",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "name": "secret_id",
            "in": "path",
            "required": true,
            "description": "Secret ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "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",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "name": "secret_id",
            "in": "path",
            "required": true,
            "description": "Secret ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "force",
            "in": "query",
            "required": false,
            "description": "Delete the secret even when AI providers reference it, destroying those providers too. Without it a referenced secret answers SECRET_HAS_DEPENDENTS.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "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.\n",
        "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": "project_id",
            "in": "query",
            "required": false,
            "description": "Filter by project public ID. Required for `soat:ResourceTag` policy conditions to narrow the listing: a policy is read per project, so without it the listing is scoped to the projects the caller can access and nothing further.\n",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "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. Repeat the parameter to OR values. A value outside the enum, empty string included, is a `400`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "open",
                  "closed",
                  "expired"
                ]
              }
            },
            "style": "form",
            "explode": true
          },
          {
            "$ref": "#/components/parameters/TagsQuery"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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, including a `usage` roll-up of what its generations cost. The listing omits `usage`; for a window, a split by day or model, or one end user across every session, narrow the usage aggregate with `session_id` / `actor_id` instead.",
        "operationId": "getSession",
        "x-soat-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "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.\n",
        "operationId": "deleteSession",
        "x-soat-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "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 `auto_generate` is enabled on the session and no generation is currently in progress, generation is triggered automatically and the response is the generated turn, the same body `?wait=true` generate answers. Otherwise returns the saved user message.\n",
        "operationId": "addSessionMessage",
        "x-soat-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "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"
          },
          "409": {
            "description": "`IDEMPOTENCY_KEY_REUSED` — the key is already claimed by a message in this session sent from a different request. No message is saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`QUOTA_EXCEEDED`: the session auto-generates and an `enforce`-mode generation quota is exhausted. `error.meta` carries `quota_id`, `metric`, `limit`, `window` and `resets_at`, with a `Retry-After` header. The message is saved. A session with `message_delay_seconds` answers `201` before the quota is read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/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.\n",
        "operationId": "generateSessionResponse",
        "x-soat-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          },
          {
            "name": "wait",
            "in": "query",
            "required": false,
            "x-soat-tool-forced": true,
            "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. A tool call, `builtin` or MCP, always waits.",
            "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/GenerateConversationMessageResponse"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "429": {
            "description": "`QUOTA_EXCEEDED`: an `enforce`-mode generation quota is exhausted. `error.meta` carries `quota_id`, `metric`, `limit`, `window` and `resets_at`, with a `Retry-After` header. Only with `?wait=true`: a background call is answered `202` before the quota is read.",
            "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}.\n",
            "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. A generation that did not run in the session's conversation is 404 (GENERATION_NOT_FOUND) and receives no outputs.\n",
        "operationId": "submitSessionToolOutputs",
        "x-soat-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "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/GenerateConversationMessageResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The generation is not paused on client tool calls (GENERATION_NOT_AWAITING_TOOL_OUTPUTS): it never paused, or its outputs were already submitted. Each pause accepts outputs once.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/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.\n\nThe 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.\n\nThe 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.\n",
        "operationId": "forkSession",
        "x-soat-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "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.\n",
        "operationId": "listSessionForks",
        "x-soat-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
        "x-soat-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "responses": {
          "200": {
            "description": "Session tags",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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",
        "x-soat-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated tags",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "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",
        "x-soat-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagBag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated tags",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagBag"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/shares": {
      "post": {
        "tags": [
          "Shares"
        ],
        "summary": "Create a share",
        "description": "Offers one of the project's resources to another project — or, with `grantee: \"*\"`, to every project — with the listed actions. The share grants nothing until the grantee accepts it. `grantee: \"*\"` is refused with `403 PUBLIC_SHARES_DISABLED` unless the deployment sets `SHARES_ALLOW_PUBLIC=true`.",
        "operationId": "createShare",
        "x-soat-agent-exclude": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "resource",
                  "actions",
                  "grantee"
                ],
                "additionalProperties": false,
                "properties": {
                  "project_id": {
                    "x-soat-ref": "projects",
                    "type": "string",
                    "description": "The publishing project (required if not using project key auth)",
                    "example": "proj_V1StGXR8Z5jdHi6B"
                  },
                  "resource": {
                    "type": "string",
                    "description": "The SRN of one resource in the publishing project, `srn:<project_id>:<type>:<id>`. No wildcard.",
                    "example": "srn:proj_V1StGXR8Z5jdHi6B:tool:tool_V1StGXR8Z5jdHi6B"
                  },
                  "actions": {
                    "type": "array",
                    "minItems": 1,
                    "description": "The actions granted: `tools:CallTool` for a `tool`, `agents:CreateAgentGeneration` for an `agent`. Any other action is `400 VALIDATION_FAILED`.",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "tools:CallTool"
                    ]
                  },
                  "grantee": {
                    "type": "string",
                    "description": "The project the share is offered to, or `*` for every project. Never the publishing project itself.",
                    "example": "proj_Q1StGXR8Z5jdHi6B"
                  },
                  "cap": {
                    "description": "The calls each accepting project may make through the share per fixed window; a call past it is `429 SHARE_CAP_EXCEEDED`. Omit or `null` for no cap.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/ShareCap"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Share created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Share"
                }
              }
            }
          },
          "400": {
            "description": "The resource is not one shareable SRN in the project, an action is not grantable for its type, the grantee is invalid, or the resource does not exist (`TOOL_NOT_FOUND` / `AGENT_NOT_FOUND`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden, or a public share on a deployment that disables them",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Shares"
        ],
        "summary": "List shares",
        "description": "With `role=publisher` (the default), the shares the project published. With `role=grantee`, the shares addressed to the project and the public shares it accepted, each with the project's own `acceptance`. A public share the project never accepted is not listed.",
        "operationId": "listShares",
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "description": "Project ID (required if not using project key auth, and always for `role=grantee`)",
            "schema": {
              "type": "string",
              "example": "proj_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "role",
            "in": "query",
            "description": "Which side of the shares to list",
            "schema": {
              "type": "string",
              "enum": [
                "publisher",
                "grantee"
              ],
              "default": "publisher"
            }
          },
          {
            "name": "resource",
            "in": "query",
            "description": "Only shares of this resource SRN (`role=publisher` only)",
            "schema": {
              "type": "string",
              "example": "srn:proj_V1StGXR8Z5jdHi6B:tool:tool_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of shares",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Share"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`role=grantee` with no project",
            "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/shares/{share_id}": {
      "get": {
        "tags": [
          "Shares"
        ],
        "summary": "Get a share",
        "description": "Returns a share with the grantee's view of the resource in `projection`. The publisher reads any of its shares; a grantee project, named by `project_id` or by its scoped credential, reads a share addressed to it or a public one, with its own `acceptance`.",
        "operationId": "getShare",
        "parameters": [
          {
            "$ref": "#/components/parameters/share_id"
          },
          {
            "name": "project_id",
            "in": "query",
            "description": "The project the caller reads as. Omit, or name the publisher, for the publisher's view; name a grantee project for its view.",
            "schema": {
              "type": "string",
              "example": "proj_Q1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Share details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Share"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Share not found, or not addressed to the project the caller reads as",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Shares"
        ],
        "summary": "Update a share",
        "description": "Changes a share's `cap`, as its publisher. The next call through the share reads the new cap; a changed `window` starts a fresh count.",
        "operationId": "updateShare",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/share_id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "cap": {
                    "description": "The calls each accepting project may make through the share per fixed window; a call past it is `429 SHARE_CAP_EXCEEDED`. `null` removes the cap.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/ShareCap"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated share",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Share"
                }
              }
            }
          },
          "400": {
            "description": "Invalid cap",
            "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": "Share not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Shares"
        ],
        "summary": "Delete a share",
        "description": "Deletes a share and every acceptance of it. Each project whose acceptance was active gets a `share_revoked` activity entry.",
        "operationId": "deleteShare",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/share_id"
          }
        ],
        "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": "Share not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/shares/{share_id}/accept": {
      "post": {
        "tags": [
          "Shares"
        ],
        "summary": "Accept a share",
        "description": "Records the grantee project's acceptance, which is what makes the share grant anything. Idempotent. A project that revoked its own acceptance may accept again; one whose acceptance the publisher revoked answers `403 SHARE_REVOKED` until the publisher deletes that acceptance. A suspended share can be accepted and grants nothing until resumed.",
        "operationId": "acceptShare",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/share_id"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "project_id": {
                    "x-soat-ref": "projects",
                    "type": "string",
                    "description": "The accepting project (required if not using project key auth)",
                    "example": "proj_Q1StGXR8Z5jdHi6B"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The project's acceptance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareAcceptance"
                }
              }
            }
          },
          "400": {
            "description": "No accepting project, or the publisher accepting its own share",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden, or `SHARE_REVOKED`: the share, or this project's acceptance, was revoked by the publisher",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Share not found, or not addressed to the project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/shares/{share_id}/suspend": {
      "post": {
        "tags": [
          "Shares"
        ],
        "summary": "Suspend a share",
        "description": "Sets `suspended_at`, which denies every acceptance without removing any. Reversible with resume; nobody re-accepts. Each project with an active acceptance gets a `share_suspended` activity entry. Idempotent.",
        "operationId": "suspendShare",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/share_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Share suspended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Share"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden, or `SHARE_REVOKED`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Share not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/shares/{share_id}/resume": {
      "post": {
        "tags": [
          "Shares"
        ],
        "summary": "Resume a share",
        "description": "Clears `suspended_at`. Each project with an active acceptance gets a `share_resumed` activity entry. Idempotent.",
        "operationId": "resumeShare",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/share_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Share resumed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Share"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden, or `SHARE_REVOKED`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Share not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/shares/{share_id}/revoke": {
      "post": {
        "tags": [
          "Shares"
        ],
        "summary": "Revoke a share",
        "description": "As the publisher, revokes the whole share and every acceptance of it; terminal. Each project whose acceptance was active gets a `share_revoked` activity entry. As a grantee (`project_id`, or a credential scoped to it), revokes that project's own acceptance, which it may accept again later; while the project's own resources still name the shared resource (see `GET /api/v1/shares/{share_id}/references`), that answers `409 SHARE_IN_USE` unless `force=true`. A publisher's revoke is never refused on that account. Idempotent.",
        "operationId": "revokeShare",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/share_id"
          },
          {
            "name": "force",
            "in": "query",
            "required": false,
            "description": "As a grantee, revoke even while the project's resources name the shared resource; they degrade as for a revoked share.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "project_id": {
                    "x-soat-ref": "projects",
                    "type": "string",
                    "description": "The project the caller revokes for. Omit, or name the publisher, to revoke the share; name a grantee project to revoke its acceptance.",
                    "example": "proj_Q1StGXR8Z5jdHi6B"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The share after the revoke, with the grantee's `acceptance` when a grantee revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Share"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Share not found, not addressed to the project, or never accepted by it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "`SHARE_IN_USE`: the grantee's resources still name the shared resource. `error.meta.references` lists them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/shares/{share_id}/references": {
      "get": {
        "tags": [
          "Shares"
        ],
        "summary": "List a grantee's references to a shared resource",
        "description": "The grantee project's own resources that name the resource the share grants: agents binding a shared tool, pipelines stepping through it, ingestion rules converting with it, orchestrations, triggers and formations naming it. What degrades if the project revokes its acceptance, and what `409 SHARE_IN_USE` lists. The grantee is named by `project_id` or by its scoped credential; the publisher holds none of these and is answered `400`.",
        "operationId": "listShareReferences",
        "parameters": [
          {
            "$ref": "#/components/parameters/share_id"
          },
          {
            "name": "project_id",
            "in": "query",
            "description": "The grantee project whose references to list.",
            "schema": {
              "type": "string",
              "example": "proj_Q1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "The grantee's references",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ShareReference"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The caller acts as the publisher",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Share not found, or not addressed to the project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/shares/{share_id}/acceptances": {
      "get": {
        "tags": [
          "Shares"
        ],
        "summary": "List a share's acceptances",
        "description": "The projects that accepted the share, for its publisher.",
        "operationId": "listShareAcceptances",
        "parameters": [
          {
            "$ref": "#/components/parameters/share_id"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only acceptances in these statuses. Repeat the parameter to OR values. A value outside the enum, empty string included, is a `400`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "active",
                  "revoked"
                ]
              }
            },
            "style": "form",
            "explode": true
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of acceptances",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ShareAcceptance"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Share not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/shares/{share_id}/acceptances/{acceptance_id}/revoke": {
      "post": {
        "tags": [
          "Shares"
        ],
        "summary": "Revoke an acceptance",
        "description": "The publisher cutting one consumer. The acceptance is kept with `revoked_by: publisher`, and a new accept from that project answers `403 SHARE_REVOKED` until the publisher deletes it. The project gets a `share_revoked` activity entry if the acceptance was active.",
        "operationId": "revokeShareAcceptance",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/share_id"
          },
          {
            "$ref": "#/components/parameters/acceptance_id"
          }
        ],
        "responses": {
          "200": {
            "description": "The revoked acceptance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareAcceptance"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Share or acceptance not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/shares/{share_id}/acceptances/{acceptance_id}": {
      "delete": {
        "tags": [
          "Shares"
        ],
        "summary": "Delete an acceptance",
        "description": "Removes one project's acceptance, which lets a project the publisher revoked accept again. The project gets a `share_revoked` activity entry if the acceptance was active.",
        "operationId": "deleteShareAcceptance",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/share_id"
          },
          {
            "$ref": "#/components/parameters/acceptance_id"
          }
        ],
        "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": "Share or acceptance not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tasks": {
      "get": {
        "description": "Lists tasks (the board query). Filter by workflow, state, status, automation status, assignee, or `metadata` — `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,
            "description": "Filter by whether the task is open or closed. Repeat the parameter to OR values. A value outside the enum, empty string included, is a `400`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "open",
                  "closed"
                ]
              }
            },
            "style": "form",
            "explode": true
          },
          {
            "name": "automation_status",
            "in": "query",
            "required": false,
            "description": "Filter by the current state's dispatch status. Repeat the parameter to OR values. `none` selects the tasks whose `automation_status` is `null` — the ones that never entered a state with an automation. It is a value a task really holds, so it is a value of the filter too; the parameter's own absence already means \"every task\".\n\nA value outside the enum, empty string included, is a `400`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "running",
                  "completed",
                  "failed",
                  "unrouted",
                  "paused",
                  "none"
                ]
              }
            },
            "style": "form",
            "explode": true,
            "example": [
              "running"
            ]
          },
          {
            "name": "assignee",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/MetadataQuery"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTaskRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The idempotency key already names a task created by the same request; that task is returned as it is now and no second task is created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "201": {
            "description": "Task created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — invalid payload (`TASK_PAYLOAD_INVALID`), `state` does not name a declared state of the workflow (`TASK_STATE_NOT_FOUND`), or an invalid idempotency key (`VALIDATION_FAILED`)"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Workflow not found"
          },
          "409": {
            "description": "The idempotency key names a task created by a different request (`IDEMPOTENCY_KEY_REUSED`)"
          }
        }
      }
    },
    "/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": {
      "get": {
        "description": "Lists every transition of the workflow version the task is pinned to, with whether firing it now as the caller would be accepted and, when not, the error code firing it would answer. The guard runs with the caller as `principal`. A `requires_approval` transition is allowed before its guard passes: the guard runs when the approval resolves. Advisory: firing re-validates under the task's lock.",
        "tags": [
          "Tasks"
        ],
        "summary": "List a task's transitions",
        "operationId": "listTaskTransitions",
        "parameters": [
          {
            "name": "task_id",
            "in": "path",
            "required": true,
            "description": "The task whose transitions are listed.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "The pinned version's transitions, in definition order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TaskTransitionOption"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Task not found"
          }
        }
      },
      "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"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "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, the task is closed, or the idempotency key names a different request"
          }
        }
      }
    },
    "/api/v1/tasks/{task_id}/migrate": {
      "post": {
        "description": "Moves an open task onto a newer version of its workflow. A state the target version also declares maps to itself; `state_map` maps a state it lacks. The payload is re-validated against the target `payload_schema`. The mapped state's `on_enter` is not fired: transition the task when you want the target's automation. A task with a dispatch in flight or a transition pending approval is refused, since both belong to the version they started under. Recorded in history as a row with `transition: null` and both versions.",
        "tags": [
          "Tasks"
        ],
        "summary": "Migrate a task to a newer workflow version",
        "operationId": "migrateTask",
        "parameters": [
          {
            "name": "task_id",
            "in": "path",
            "required": true,
            "description": "The task to migrate.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MigrateTaskRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The task on the target version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "400": {
            "description": "The version does not exist or is not newer than the task's, the current state is unmapped, a mapped state is undeclared or terminal, or the payload fails the target `payload_schema`"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Task not found"
          },
          "409": {
            "description": "The task is closed, has a transition pending approval, or has a dispatch in flight"
          }
        }
      }
    },
    "/api/v1/tasks/{task_id}/pause": {
      "post": {
        "description": "Pauses a task's automation. While a pause is in force no state's `on_enter` dispatches and no retry chain continues — an agent generation, a tool call and a sub-orchestration alike — so the task stops spending without losing its place.\nA workflow has no run object, so this is the workflow half of pause-orchestration-run: the pause lands on the instance, which is the task. Transitions are deliberately still allowed — a move costs nothing while every dispatch it would start is suppressed — so a board stays usable under a pause. Entering a state whose dispatch is suppressed records `automation_status: paused`, which resume-task reads to know that state still owes its work.\nA dispatch already in flight is left to finish, and its outcome still routes; only what would start after it is suppressed.\nIdempotent: pausing an already-paused task answers with it unchanged.",
        "tags": [
          "Tasks"
        ],
        "summary": "Pause a task",
        "operationId": "pauseTask",
        "parameters": [
          {
            "name": "task_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PauseTaskRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The paused task",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Task not found"
          },
          "409": {
            "description": "The task is closed, so it has no automation left to pause"
          }
        }
      }
    },
    "/api/v1/tasks/{task_id}/resume": {
      "post": {
        "description": "Lifts a task's pause. When the pause suppressed the current state's `on_enter` — `automation_status: paused` — that dispatch is started now, as the caller resuming rather than as whoever last moved the task. A state whose dispatch had already completed, or that declares none, is left alone, so a resume never re-spends work the pause did not stop.\nThis is the only way a pause is lifted; a task that is merely idle is advanced by firing a transition instead.",
        "tags": [
          "Tasks"
        ],
        "summary": "Resume a task",
        "operationId": "resumeTask",
        "parameters": [
          {
            "name": "task_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resumed task",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Task not found"
          },
          "409": {
            "description": "The task carries no pause to lift"
          }
        }
      }
    },
    "/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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "The task's transition history, oldest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TaskTransition"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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. A credential scoped to a project the tool is shared with, through an accepted share, reads its `id`, `name`, `description` and `parameters` only.\n",
        "operationId": "getTool",
        "x-soat-resource": {
          "kind": "tool",
          "from": "tool_id"
        },
        "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",
        "x-soat-resource": {
          "kind": "tool",
          "from": "tool_id"
        },
        "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. A tool that is a decider's backend is refused with `409 TOOL_HAS_DEPENDENTS`, and so is one another project has accepted a share of, or one of its own project's ingestion rules converts with, until `force=true`. Every share of the tool is revoked when it is deleted; an ingestion rule keeps its `tool_id` and fails the documents it matches with `CONVERTER_FAILED` until repointed.",
        "operationId": "deleteTool",
        "x-soat-resource": {
          "kind": "tool",
          "from": "tool_id"
        },
        "parameters": [
          {
            "name": "tool_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "force",
            "in": "query",
            "required": false,
            "description": "Delete the tool even when another project has accepted a share of it, revoking those shares, or an ingestion rule converts with it. A decider backend still refuses.",
            "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": "The tool is a decider's backend, or has accepted shares and `force` is not set",
            "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.\nFor `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.\n`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.\nGuardrails attached to the tool or to its project adjudicate the call before dispatch, composing project + tool scope. A call this route cannot await a decision on — class C (human sign-off), class D, or a class-B tripwire — is refused with `422 TOOL_DISPATCH_FAILED`, whose `meta` carries the `tool_id` and the `outcome`. A `pipeline` tool is adjudicated before its first step runs, and every step is adjudicated as the call of that tool it is.\n\nA credential scoped to a project the tool is shared with, through an accepted share, calls it in that project: the calling project's guardrails adjudicate it, it is metered there with `publisher_project_id`, and the tool receives the calling project as the `calling_project_id` tool context key.\n",
        "operationId": "callTool",
        "x-soat-resource": {
          "kind": "tool",
          "from": "tool_id"
        },
        "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.\n",
            "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"
                }
              }
            }
          },
          "429": {
            "description": "`SHARE_CAP_EXCEEDED`: the tool is another project's, reached through a share whose `cap` is spent for the window. Carries a `Retry-After` header and `error.meta.retry_after`.",
            "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`. A target that never answers (`http` or `mcp`) is `TOOL_UNREACHABLE`, its message naming the network failure and its `meta` the `tool_url` and `tool_method`.\n",
            "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": "parent_trace_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "trace_V1StGXR8Z5jdHi6B"
            },
            "description": "Only the direct children of this trace. A trace naming nothing the caller can read matches nothing rather than widening the listing."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
        "x-soat-resource": {
          "kind": "trace",
          "from": "trace_id"
        },
        "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. A node's generations are listed by `GET /api/v1/generations?trace_id=`, and a node's direct children page by `GET /api/v1/traces?parent_trace_id=`.\n",
        "operationId": "getTraceTree",
        "x-soat-resource": {
          "kind": "trace",
          "from": "trace_id"
        },
        "parameters": [
          {
            "name": "trace_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of any trace in the tree (root or child)"
          }
        ],
        "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.\n\nThe 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.\n\nIdempotent — purging an already-purged trace succeeds and leaves the original `content_redacted_at` in place.\n",
        "operationId": "purgeTraceContent",
        "x-soat-resource": {
          "kind": "trace",
          "from": "trace_id"
        },
        "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"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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. Changing `target_type` or `target_id` re-checks the target-start action (`orchestrations:StartRun`, `agents:CreateAgentGeneration` or `tools:CallTool`) against the resulting target, because a firing runs with the trigger creator's authority rather than the updater's. A caller who could not start the new target is answered `403` and the trigger keeps the target it had.\n",
        "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",
        "x-soat-agent-exclude": true,
        "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",
        "x-soat-agent-exclude": true,
        "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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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/events": {
      "get": {
        "tags": [
          "Usage"
        ],
        "summary": "List usage events",
        "description": "Returns the raw usage events the caller can access, most recent first, narrowed by any combination of the same filters the aggregate takes — agent, generation, trace, actor, session, AI provider, orchestration run, orchestration, tool, trigger, action, outcome, meter type, model or `source` — so a rollup and the events behind it are addressed the same way. Each event is the per-generation token usage as reported by the provider, for audit and reconciliation.\n\nAn id filter matches the id the event recorded, so it still selects the spend of an agent, provider, session or tool since deleted. An id no event in scope recorded yields an empty page rather than dropping the filter, so a mistyped narrowing can never widen the list past what was asked for.\n",
        "operationId": "listUsageEvents",
        "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.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "session_id",
            "in": "query",
            "required": false,
            "description": "Filter by the session the usage was produced in.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ai_provider_id",
            "in": "query",
            "required": false,
            "description": "Filter by the provider record the spend was billed against.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orchestration_run_id",
            "in": "query",
            "required": false,
            "description": "Filter by the orchestration run that metered the event.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orchestration_id",
            "in": "query",
            "required": false,
            "description": "Filter by orchestration — the events of the runs it started itself, not the subtree its `loop` or `sub_orchestration` nodes started. An orchestration that does not exist in scope yields an empty page.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "model",
            "in": "query",
            "required": false,
            "description": "Filter by the as-billed SKU the event recorded. Matched as recorded, so it still selects events whose provider has since been deleted.\n",
            "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": "tool_id",
            "in": "query",
            "required": false,
            "description": "Filter by the tool a `tool_execution` event metered, including a tool another project shares with one in scope.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "publisher_project_id",
            "in": "query",
            "required": false,
            "description": "Filter to the calls made through a share of a resource the named project owns.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "outcome",
            "in": "query",
            "required": false,
            "description": "Filter by how a `tool_execution` call ended: `ok`, `error` or `timeout`. Every other meter carries no outcome and is matched by none of them.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "meter_type",
            "in": "query",
            "required": false,
            "description": "Filter by meter type (e.g. `llm_tokens`, `compute_execution`, `api_request`, `storage`, `tool_execution`)\n",
            "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, `eval_judge` an `llm_judge` scorer's own completion and `eval_scorer` a `tool` scorer's call, so verification spend is `source` in (`eval`, `eval_judge`, `eval_scorer`); `decider` is a decision's evaluation; `embedding` is any embedding call. Ordinary agent traffic carries no source and is matched by none of them.\n",
            "schema": {
              "type": "string",
              "example": "eval"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of usage events",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/UsageEvent"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request — a query parameter this endpoint does not declare. An unrecognised filter is rejected rather than ignored, since ignoring one answers a narrow question with the unfiltered list.",
            "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/aggregate": {
      "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, optionally bucketed by a single dimension and narrowed by any combination of sixteen filters — session, actor, agent, AI provider, orchestration run, orchestration, generation, trace, tool, publisher project, meter type, model, source, trigger, action and outcome. That is how \"what did this conversation cost\", \"what has this end user cost\" and \"what did this agent cost\" are answered. Each group and the grand total carry an `event_count`, 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 usage events client-side.\n\nNarrowings intersect and apply to the whole rollup — every bucket, `totals` and `totals.distinct` alike — so each composes with any `group_by`, and all sixteen are echoed back under `filters`. The ten naming a resource match the id the event recorded, so a deleted resource's spend stays selectable; an id no event recorded empties the rollup rather than being dropped: a mistyped id must never read back as the project's whole spend. `orchestration_id` is the exception — the event records no orchestration, so a deleted one empties the rollup. The other six are matched against the value the event recorded.\n\n`groups` is paginated. Its `total` is the number of distinct buckets in the window — bucket cardinality, including a null bucket for the events the dimension does not apply to, and never a count of entities. To count entities, send `include=distinct` and read `totals.distinct`. `totals` always describes the whole window — never the page above it, and never the bucketing: omit `group_by` and `totals` is unchanged while `groups` is empty.\n",
        "operationId": "getUsageAggregate",
        "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": false,
            "description": "Dimension to bucket by. Omit it to skip bucketing entirely: `totals` still describes the whole window and `groups` is an empty page, which is what \"what did this one thing cost\" wants. A value naming no dimension is a `400`. `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. `orchestration_run` buckets on the orchestration run the event belongs to — a standalone generation runs no orchestration and collapses into the single `null` bucket, so the bucket count under this dimension is not a count of runs.\n",
            "schema": {
              "$ref": "#/components/schemas/UsageGroupBy"
            }
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Optional extras to compute. The only value is `distinct`, which adds `totals.distinct` — one distinct-entity count per attribution column. It is opt-in because each key costs the database one extra sort of the window, so the default response keeps its cost whatever the event table grows to carry; a caller counting entities sends one request at `limit=1&include=distinct`. Any other value is a `400`.\n",
            "schema": {
              "type": "string",
              "enum": [
                "distinct"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Inclusive lower bound (ISO-8601 timestamp) on the event created_at. Omit for no lower bound.\n",
            "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.\n",
            "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`, `tool_execution`). Omit to include every meter. An unknown type yields an empty rollup, not an error.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "session_id",
            "in": "query",
            "required": false,
            "description": "Narrow the whole rollup — every bucket, `totals` and `totals.distinct` alike — to one session's traffic. Combines with `group_by`: `group_by=day&session_id=…` is a conversation's spend per day, `group_by=model` is the same spend split by model. Like every id narrowing it matches the id the event recorded, so a deleted session's spend stays selectable; an id no event in this project recorded yields an empty rollup, never the project total.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actor_id",
            "in": "query",
            "required": false,
            "description": "The same narrowing, for one end user across every session they appear in. This is the figure behind a `scope: actor`, `metric: cost_usd` quota — what that actor has spent, against the cap it is held to.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "agent_id",
            "in": "query",
            "required": false,
            "description": "Narrow to one agent's traffic, across every session, run and trigger that dispatched it.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ai_provider_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the spend billed against one provider record — a routed generation's serving target, or the agent's pinned provider.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orchestration_run_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the events one orchestration run metered.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orchestration_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the runs of one orchestration — the runs it started itself, never the subtree a `loop` or `sub_orchestration` node started under it, which is metered against the child orchestration where it was incurred. Additive: summed across a project's orchestrations the figures reach the project total exactly once. For the subtree figure of a single invocation, read `usage` on the run.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "generation_id",
            "in": "query",
            "required": false,
            "description": "Narrow to one generation's events. A generation writes more than one when it meters several dimensions (tokens and compute).\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "trace_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the events recorded under one trace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "model",
            "in": "query",
            "required": false,
            "description": "Narrow to one model, matched against the as-billed SKU the event recorded. A value naming no model yields an empty rollup, not an error. Under `group_by=model` this is a single-bucket rollup; under any other dimension it is that model's spend split by the dimension.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "Narrow by what the spend was incurred for — `eval` and `eval_judge` are verification spend, `decider` is decision spend. Ordinary traffic carries no source, so it cannot be selected by this filter; omit the filter for everything.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "trigger_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the spend one trigger initiated. Matched as recorded rather than resolved, so it still selects the spend of a trigger that has since been deleted.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "action_id",
            "in": "query",
            "required": false,
            "description": "Narrow to one caller-supplied action label, for per-action spend. Matched as recorded, like `trigger_id`.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tool_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the executions of one tool, including one another project shares with this one.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "publisher_project_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the calls made through a share of a resource the named project owns.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "outcome",
            "in": "query",
            "required": false,
            "description": "Narrow `tool_execution` events by how the call ended — `ok`, `error` or `timeout` — matched as recorded. `outcome=error` is a tool's failures without a second meter.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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, or a query parameter this endpoint does not declare — an unrecognised filter is rejected rather than ignored, since ignoring one returns the project-wide total in answer to a narrower question)",
            "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_thresholds.crossed` webhook when a project's cost or token usage over a calendar-month or rolling-24h window crosses the configured value.\n",
        "operationId": "listUsageThresholds",
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "description": "Filter by project public ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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).\n",
        "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.\n",
        "operationId": "deleteUsageThreshold",
        "x-soat-resource": {
          "kind": "usage",
          "from": "threshold_id"
        },
        "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 orchestration-run billing receipt",
        "description": "Returns a billing receipt. Pass generation_id for a per-generation receipt, or orchestration_run_id for a receipt summed across the orchestration run's events — 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.\n",
        "operationId": "getUsageReceipt",
        "parameters": [
          {
            "name": "generation_id",
            "in": "query",
            "required": false,
            "description": "Generation public ID. Mutually exclusive with orchestration_run_id.\n",
            "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.\n",
            "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.\n",
        "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.\n",
        "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",
        "x-soat-agent-exclude": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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",
        "x-soat-agent-exclude": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "username",
                  "password"
                ],
                "additionalProperties": false,
                "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",
        "x-soat-agent-exclude": true,
        "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",
        "x-soat-agent-exclude": true,
        "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",
        "x-soat-agent-exclude": true,
        "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",
        "x-soat-agent-exclude": true,
        "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",
        "x-soat-agent-exclude": true,
        "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"
                ],
                "additionalProperties": false,
                "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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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.\n\nA new delivery record is created rather than the original being reset,\nso the original attempt stays in the history. The send happens in the\nbackground: poll the returned delivery to observe its outcome.\n",
        "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",
        "x-soat-agent-exclude": true,
        "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",
        "x-soat-agent-exclude": true,
        "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"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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"
            }
          },
          {
            "$ref": "#/components/parameters/IfMatchVersion"
          }
        ],
        "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"
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      },
      "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).\n",
        "tags": [
          "Workflows"
        ],
        "summary": "List a workflow's versions",
        "operationId": "listWorkflowVersions",
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "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 runs on in `workflow_version`, fixed at creation and moved only by migrate-task, so this is how you read the definition a task is actually being validated against — including a task whose workflow has been rewired since.\n",
        "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.\n\nThe 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.\n",
        "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"
          },
          "409": {
            "$ref": "#/components/responses/VersionConflict"
          }
        }
      }
    },
    "/api/v1/workflows/{workflow_id}/migrations": {
      "post": {
        "description": "Moves every open task of the workflow that runs on an older version, or on none, onto `to_version`. Each task is migrated as migrate-task migrates one and needs `tasks:MigrateTask` on it; a task it would refuse is reported as `refused` with the error, and the run goes on. The tasks and the caller's permission on each are fixed when the run is recorded. A `state_map` target the version lacks, or a terminal one, refuses the request before any task moves. Runs in the background by default (`status: queued`); `wait: true` answers once every task has settled. See [Migrating every open task](/docs/modules/workflows#migrating-every-open-task).",
        "tags": [
          "Workflows"
        ],
        "summary": "Migrate a workflow's open tasks to one of its versions",
        "operationId": "createWorkflowMigration",
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "description": "The workflow whose tasks migrate.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWorkflowMigrationRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The run, `queued` in the background or `completed` with `wait: true`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkflowMigration"
                }
              }
            }
          },
          "400": {
            "description": "The version does not exist, or a `state_map` target is undeclared or terminal"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Workflow not found"
          }
        }
      }
    },
    "/api/v1/workflows/{workflow_id}/migrations/{migration_id}": {
      "get": {
        "description": "Returns a workflow migration with the outcome of each task.",
        "tags": [
          "Workflows"
        ],
        "summary": "Get a workflow migration",
        "operationId": "getWorkflowMigration",
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "description": "The workflow whose tasks migrate.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "migration_id",
            "in": "path",
            "required": true,
            "description": "The workflow migration to read.",
            "schema": {
              "type": "string",
              "example": "wfl_mig_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The workflow migration",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkflowMigration"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Workflow or migration 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_created",
              "approval_resolved",
              "exception_created",
              "schedule_fired",
              "share_cap_exceeded",
              "share_resumed",
              "share_revoked",
              "share_suspended",
              "tool_resolution_failed",
              "usage_quantity_invalid"
            ],
            "description": "How the entry was produced. `tool_resolution_failed` means a tool binding contributed no tool to the turn — its source could not be reached or refused the listing, or the listing was read and left nothing to attach — so the turn ran without it. The generation itself completes, carrying no error, so this is the only signal that it answered with fewer tools than it was configured to have. `share_suspended`, `share_resumed` and `share_revoked` land in a consumer project when the publisher of a share it accepted suspends, resumes or revokes it; `ref_id` is the share and `detail` names the resource and the publisher project. `share_cap_exceeded` lands in the consumer project when a call through a share it accepted is refused by the share's `cap`. `usage_quantity_invalid` lands in the project a tool call is metered in when a resource price row's `quantity` reads no finite number >= 0; `ref_id` is the tool."
          },
          "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, 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"
          },
          "generation_id": {
            "type": "string",
            "nullable": true,
            "description": "The agent generation the entry was produced during, if any. Filter on it with the `generation_id` query parameter to read everything one turn did."
          },
          "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": {
            "$ref": "#/components/schemas/TagBag"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "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/ToolBindingRead"
            },
            "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": {
            "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_store_ids": {
                "x-soat-ref": "memory-stores",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "document_ids": {
                "x-soat-ref": "documents",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "document_paths": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "tags": {
                "description": "Key-value pairs a result's own `tags` must all contain (exact match). Scopes documents and memories alike.",
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TagBag"
                  }
                ]
              },
              "min_score": {
                "type": "number",
                "description": "Minimum raw cosine similarity (0–1) a vector candidate must reach to be ranked, the floor the search endpoint spells `min_similarity`. No default: omitted means no floor at all, and every one of the `limit` nearest chunks is injected however weak it is.\n"
              },
              "rrf_k": {
                "type": "integer",
                "description": "The `k` in the fusion term `1 / (k + rank)`, the same knob knowledge search takes. Smaller weights the top of each ranking more heavily. Omitted, the deployment's `KNOWLEDGE_RRF_K` applies.\n"
              },
              "recency_half_life_days": {
                "type": "number",
                "description": "Half-life in days of the decay applied to **memory** results after fusion, the same knob knowledge search takes. `0` disables it. Omitted, the deployment's `KNOWLEDGE_RECENCY_HALF_LIFE_DAYS` applies.\n"
              },
              "limit": {
                "type": "integer",
                "description": "Maximum number of results to inject. Omitted, 10 are injected.\n"
              },
              "write_memory_store_id": {
                "x-soat-ref": "memory-stores",
                "type": "string",
                "nullable": true,
                "description": "Public ID of the memory store the agent can write to during generation. When set, a write_memory tool is automatically available to the agent."
              }
            }
          },
          "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."
          },
          "prompt_caching": {
            "type": "object",
            "nullable": true,
            "description": "Prompt caching for this agent's turns. `{\"enabled\": true}` marks two cache breakpoints: the end of the system block, which covers the tool definitions and the instructions, and the last message of each step, which covers the conversation so far. A provider that caches by explicit breakpoint then reads, on every later step and every later turn of the session, the whole prefix the previous one sent, and writes only what was appended. Null or omitted is off: a cache write costs more than an uncached token, so an agent whose prefix is never re-read would pay for the privilege. A prefix below the model's minimum cacheable length is not cached. Cache reads are reported as `cached_tokens` and cache writes as `cache_write_tokens` on usage; a write with no `cache_write_tokens` price bills at the input rate.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Whether the breakpoint is marked. Defaults to false."
              }
            }
          },
          "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."
          },
          "max_output_tokens": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Maximum number of tokens the model may generate per step of a turn. A response that reaches it stops with `stop_reason` `length`. Null leaves the model's default.",
            "example": 1024
          },
          "reasoning": {
            "type": "object",
            "nullable": true,
            "additionalProperties": false,
            "required": [
              "effort"
            ],
            "description": "How much the model reasons before answering. Translated per provider (Anthropic thinking, Gemini thinking config, OpenAI reasoning effort) against what the model supports; a model that cannot reason ignores it. `none` turns reasoning off where the model allows it. Reasoning tokens are metered as output. Null leaves the model's default.",
            "properties": {
              "effort": {
                "type": "string",
                "enum": [
                  "none",
                  "low",
                  "medium",
                  "high"
                ],
                "example": "low"
              }
            }
          },
          "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`, `max_output_tokens`, `reasoning`, `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). A literal credential in an inline `tool_bindings` definition is masked as on the `Agent` read, and a restore or release of such a version is refused with `400 LITERAL_CREDENTIAL`.\n\nDeliberately 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 memory stores 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_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who produced this version. Null for a write with no principal behind it. A formation apply is attributed to the credential that deployed it."
          },
          "created_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RestoreAgentVersionRequest": {
        "type": "object",
        "additionalProperties": false,
        "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"
        ],
        "additionalProperties": false,
        "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",
        "additionalProperties": false,
        "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 in the agent's own project (`400 TOOL_NOT_FOUND` otherwise). Exactly one of `tool_id`/`tool`.",
            "example": "tool_V1StGXR8Z5jdHi6B"
          },
          "tool": {
            "$ref": "#/components/schemas/CreateToolRequest"
          }
        }
      },
      "ToolBindingRead": {
        "type": "object",
        "description": "One agent↔tool attachment as read back: `tool_id`, or the inline `tool` definition as written, with every literal credential masked.",
        "properties": {
          "tool_id": {
            "x-soat-ref": "tools",
            "type": "string",
            "description": "Public ID of a persisted tool in the agent's own project.",
            "example": "tool_V1StGXR8Z5jdHi6B"
          },
          "tool": {
            "type": "object",
            "additionalProperties": true,
            "description": "Inline tool definition, in the `CreateToolRequest` shape. A literal credential is refused on write and a stored one is masked as `{\"no_echo\": true}`, exactly as on a persisted tool. A `{{secret:...}}` reference stays readable."
          }
        }
      },
      "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.",
        "additionalProperties": false,
        "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": {
            "description": "Display name.",
            "type": "string"
          },
          "instructions": {
            "description": "System instructions, sent as the system message of every generation.",
            "type": "string"
          },
          "model": {
            "description": "Model identifier on the pinned AI provider. Omitted, the provider's default model is used. Cannot be combined with `model_route_id`.",
            "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": {
            "description": "Maximum agent loop steps per turn (default `20`).",
            "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": {
              "$ref": "#/components/schemas/AgentStopCondition"
            },
            "description": "Conditions that end the agent's work early, on top of `max_steps`. Two scopes:\n\n`{\"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`.\n\n`{\"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.\n\nAn 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": {
            "description": "Persisted tools from `tool_bindings` the model sees on every step. Omitted or `[]` leaves every bound tool active. See [Active Tools](/docs/modules/agents#active-tools).",
            "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": {
            "description": "Per-step overrides of `tool_choice` and `active_tool_ids`, numbered from the first step of the turn. See [Step Rules](/docs/modules/agents#step-rules).",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentStepRule"
            }
          },
          "boundary_policy": {
            "$ref": "#/components/schemas/AgentBoundaryPolicy"
          },
          "temperature": {
            "description": "Sampling temperature passed to the model. Omitted, the provider's default applies.",
            "type": "number"
          },
          "knowledge_config": {
            "description": "Knowledge search run before every generation; its matches are prepended as reference context. See [Knowledge Config](/docs/modules/agents#knowledge-config).",
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "memory_store_ids": {
                "x-soat-ref": "memory-stores",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "document_ids": {
                "x-soat-ref": "documents",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "document_paths": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "tags": {
                "description": "Key-value pairs a result's own `tags` must all contain (exact match). Scopes documents and memories alike.",
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TagBag"
                  }
                ]
              },
              "min_score": {
                "type": "number",
                "description": "Minimum raw cosine similarity (0–1) a vector candidate must reach to be ranked, the floor the search endpoint spells `min_similarity`. No default: omitted means no floor at all, and every one of the `limit` nearest chunks is injected however weak it is.\n"
              },
              "rrf_k": {
                "type": "integer",
                "description": "The `k` in the fusion term `1 / (k + rank)`, the same knob knowledge search takes. Smaller weights the top of each ranking more heavily. Omitted, the deployment's `KNOWLEDGE_RRF_K` applies.\n"
              },
              "recency_half_life_days": {
                "type": "number",
                "description": "Half-life in days of the decay applied to **memory** results after fusion, the same knob knowledge search takes. `0` disables it. Omitted, the deployment's `KNOWLEDGE_RECENCY_HALF_LIFE_DAYS` applies.\n"
              },
              "limit": {
                "type": "integer",
                "description": "Maximum number of results to inject. Omitted, 10 are injected.\n"
              },
              "write_memory_store_id": {
                "x-soat-ref": "memory-stores",
                "type": "string",
                "nullable": true,
                "description": "Public ID of the memory store the agent can write to during generation. When set, a write_memory tool is automatically available to the agent."
              }
            }
          },
          "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."
          },
          "prompt_caching": {
            "type": "object",
            "nullable": true,
            "description": "Prompt caching for this agent's turns. `{\"enabled\": true}` marks two cache breakpoints: the end of the system block, which covers the tool definitions and the instructions, and the last message of each step, which covers the conversation so far. A provider that caches by explicit breakpoint then reads, on every later step and every later turn of the session, the whole prefix the previous one sent, and writes only what was appended. Null or omitted is off: a cache write costs more than an uncached token, so an agent whose prefix is never re-read would pay for the privilege. A prefix below the model's minimum cacheable length is not cached. Cache reads are reported as `cached_tokens` and cache writes as `cache_write_tokens` on usage; a write with no `cache_write_tokens` price bills at the input rate.",
            "additionalProperties": false,
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Whether the breakpoint is marked. Defaults to false."
              }
            }
          },
          "max_context_messages": {
            "type": "integer",
            "description": "Maximum number of recent messages included in the context window. Null means no limit."
          },
          "max_output_tokens": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Maximum number of tokens the model may generate per step of a turn. A response that reaches it stops with `stop_reason` `length`. Null leaves the model's default.",
            "example": 1024
          },
          "reasoning": {
            "type": "object",
            "nullable": true,
            "additionalProperties": false,
            "required": [
              "effort"
            ],
            "description": "How much the model reasons before answering. Translated per provider (Anthropic thinking, Gemini thinking config, OpenAI reasoning effort) against what the model supports; a model that cannot reason ignores it. `none` turns reasoning off where the model allows it. Reasoning tokens are metered as output. Null leaves the model's default.",
            "properties": {
              "effort": {
                "type": "string",
                "enum": [
                  "none",
                  "low",
                  "medium",
                  "high"
                ],
                "example": "low"
              }
            }
          },
          "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).",
        "additionalProperties": false,
        "properties": {
          "ai_provider_id": {
            "description": "Public ID of the AI provider to pin. Mutually exclusive with `model_route_id`; set to `null` when switching to a route.",
            "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": {
            "description": "Display name; `null` clears it.",
            "type": "string",
            "nullable": true
          },
          "instructions": {
            "description": "System instructions, sent as the system message of every generation; `null` clears them.",
            "type": "string",
            "nullable": true
          },
          "model": {
            "description": "Model identifier on the pinned AI provider; `null` falls back to the provider's default model. Cannot be combined with `model_route_id`.",
            "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": {
            "description": "Maximum agent loop steps per turn; `null` restores the default of `20`.",
            "type": "integer",
            "nullable": true
          },
          "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",
            "nullable": true,
            "items": {
              "$ref": "#/components/schemas/AgentStopCondition"
            },
            "description": "Conditions that end the agent's work early, on top of `max_steps`. Two scopes:\n\n`{\"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`.\n\n`{\"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.\n\nAn 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": {
            "description": "Persisted tools from `tool_bindings` the model sees on every step. `null` or `[]` leaves every bound tool active. See [Active Tools](/docs/modules/agents#active-tools).",
            "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": {
            "description": "Per-step overrides of `tool_choice` and `active_tool_ids`, numbered from the first step of the turn; `null` clears them. See [Step Rules](/docs/modules/agents#step-rules).",
            "type": "array",
            "nullable": true,
            "items": {
              "$ref": "#/components/schemas/AgentStepRule"
            }
          },
          "boundary_policy": {
            "$ref": "#/components/schemas/AgentBoundaryPolicy"
          },
          "temperature": {
            "description": "Sampling temperature passed to the model; `null` restores the provider's default.",
            "type": "number",
            "nullable": true
          },
          "knowledge_config": {
            "description": "Knowledge search run before every generation; its matches are prepended as reference context. `null` turns it off. See [Knowledge Config](/docs/modules/agents#knowledge-config).",
            "type": "object",
            "nullable": true,
            "additionalProperties": false,
            "properties": {
              "memory_store_ids": {
                "x-soat-ref": "memory-stores",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "document_ids": {
                "x-soat-ref": "documents",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "document_paths": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "tags": {
                "description": "Key-value pairs a result's own `tags` must all contain (exact match). Scopes documents and memories alike.",
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TagBag"
                  }
                ]
              },
              "min_score": {
                "type": "number",
                "description": "Minimum raw cosine similarity (0–1) a vector candidate must reach to be ranked, the floor the search endpoint spells `min_similarity`. No default: omitted means no floor at all, and every one of the `limit` nearest chunks is injected however weak it is.\n"
              },
              "rrf_k": {
                "type": "integer",
                "description": "The `k` in the fusion term `1 / (k + rank)`, the same knob knowledge search takes. Smaller weights the top of each ranking more heavily. Omitted, the deployment's `KNOWLEDGE_RRF_K` applies.\n"
              },
              "recency_half_life_days": {
                "type": "number",
                "description": "Half-life in days of the decay applied to **memory** results after fusion, the same knob knowledge search takes. `0` disables it. Omitted, the deployment's `KNOWLEDGE_RECENCY_HALF_LIFE_DAYS` applies.\n"
              },
              "limit": {
                "type": "integer",
                "description": "Maximum number of results to inject. Omitted, 10 are injected.\n"
              },
              "write_memory_store_id": {
                "x-soat-ref": "memory-stores",
                "type": "string",
                "nullable": true,
                "description": "Public ID of the memory store the agent can write to during generation. When set, a write_memory tool is automatically available to the agent."
              }
            }
          },
          "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."
          },
          "prompt_caching": {
            "type": "object",
            "nullable": true,
            "description": "Prompt caching for this agent's turns. `{\"enabled\": true}` marks two cache breakpoints: the end of the system block, which covers the tool definitions and the instructions, and the last message of each step, which covers the conversation so far. A provider that caches by explicit breakpoint then reads, on every later step and every later turn of the session, the whole prefix the previous one sent, and writes only what was appended. Null or omitted is off: a cache write costs more than an uncached token, so an agent whose prefix is never re-read would pay for the privilege. A prefix below the model's minimum cacheable length is not cached. Cache reads are reported as `cached_tokens` and cache writes as `cache_write_tokens` on usage; a write with no `cache_write_tokens` price bills at the input rate.",
            "additionalProperties": false,
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Whether the breakpoint is marked. Defaults to false."
              }
            }
          },
          "max_context_messages": {
            "type": "integer",
            "nullable": true,
            "description": "Maximum number of recent messages included in the context window. Null means no limit."
          },
          "max_output_tokens": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Maximum number of tokens the model may generate per step of a turn. A response that reaches it stops with `stop_reason` `length`. Null leaves the model's default.",
            "example": 1024
          },
          "reasoning": {
            "type": "object",
            "nullable": true,
            "additionalProperties": false,
            "required": [
              "effort"
            ],
            "description": "How much the model reasons before answering. Translated per provider (Anthropic thinking, Gemini thinking config, OpenAI reasoning effort) against what the model supports; a model that cannot reason ignores it. `none` turns reasoning off where the model allows it. Reasoning tokens are metered as output. Null leaves the model's default.",
            "properties": {
              "effort": {
                "type": "string",
                "enum": [
                  "none",
                  "low",
                  "medium",
                  "high"
                ],
                "example": "low"
              }
            }
          },
          "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"
          },
          "expected_version": {
            "description": "Refuses the write unless the resource is at this version.",
            "allOf": [
              {
                "$ref": "#/components/schemas/ExpectedVersion"
              }
            ]
          }
        }
      },
      "CreateAgentGenerationRequest": {
        "type": "object",
        "required": [
          "messages"
        ],
        "additionalProperties": false,
        "properties": {
          "messages": {
            "description": "Conversation turns, oldest first. Roles are `user` and `assistant`: the system prompt is the agent's `instructions`, so a `system` entry is refused with `400 SYSTEM_MESSAGE_NOT_ALLOWED`.",
            "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": {
            "description": "Caller-supplied key/value metadata attached to the generation record for per-run audit attribution (e.g. the ticket or case this action belongs to). 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, what knowledge retrieval served) 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.",
            "allOf": [
              {
                "$ref": "#/components/schemas/NullableMetadataBag"
              }
            ]
          },
          "idempotency_key": {
            "type": "string",
            "maxLength": 255,
            "description": "Deduplication key, unique within the project, that makes a retry of an at-least-once delivery or an ambiguous failure safe. The first request under a key runs the generation; any later request carrying it runs nothing and answers `202` with the generation the key names, whatever state it has reached — including a retry that arrives while the original is still running. The key is claimed by the generation record for as long as the record exists.\n\nEvery other body field except `stream` is the request the key names: reusing a key with any of them changed, or on another agent of the project, is `409 IDEMPOTENCY_KEY_REUSED`. `stream` and `wait` say how the caller receives the generation, not what it is, so a retry may flip them.",
            "example": "discord-1287654321098765432"
          },
          "knowledge_config": {
            "type": "object",
            "nullable": true,
            "description": "Per-generation knowledge retrieval override. Array filters (memory_store_ids, document_ids, document_paths) are unioned with the agent's stored knowledge_config; `tags` pairs are merged with the override winning per key; scalar fields (min_score, rrf_k, recency_half_life_days, limit) use the per-generation value when present.",
            "additionalProperties": false,
            "properties": {
              "memory_store_ids": {
                "x-soat-ref": "memory-stores",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "document_ids": {
                "x-soat-ref": "documents",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "document_paths": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "tags": {
                "description": "Key-value pairs a result's own `tags` must all contain (exact match). Scopes documents and memories alike.",
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TagBag"
                  }
                ]
              },
              "min_score": {
                "type": "number",
                "description": "Minimum raw cosine similarity (0–1) a vector candidate must reach to be ranked, the floor the search endpoint spells `min_similarity`. No default: omitted means no floor at all, and every one of the `limit` nearest chunks is injected however weak it is.\n"
              },
              "rrf_k": {
                "type": "integer",
                "description": "The `k` in the fusion term `1 / (k + rank)`, the same knob knowledge search takes. Smaller weights the top of each ranking more heavily. Omitted, the deployment's `KNOWLEDGE_RRF_K` applies.\n"
              },
              "recency_half_life_days": {
                "type": "number",
                "description": "Half-life in days of the decay applied to **memory** results after fusion, the same knob knowledge search takes. `0` disables it. Omitted, the deployment's `KNOWLEDGE_RECENCY_HALF_LIFE_DAYS` applies.\n"
              },
              "limit": {
                "type": "integer",
                "description": "Maximum number of results to inject. Omitted, 10 are injected.\n"
              }
            }
          }
        }
      },
      "ToolOutputMessageContent": {
        "type": "object",
        "required": [
          "type",
          "tool_id"
        ],
        "additionalProperties": false,
        "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"
        ],
        "additionalProperties": false,
        "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"
        ],
        "additionalProperties": false,
        "properties": {
          "tool_outputs": {
            "description": "Results for the tool calls the paused generation is waiting on, one entry per `tool_call_id`.",
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "required": [
                "tool_call_id",
                "output"
              ],
              "additionalProperties": false,
              "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}`.\n",
        "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`.\n",
        "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.\n",
            "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"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "AgentStopCondition": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type"
        ],
        "description": "One stop condition. `has_tool_call` ends the turn after the step that calls `tool_name`; `max_chain_generations` bounds the continuation chain at `max_generations`.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "has_tool_call",
              "max_chain_generations"
            ],
            "description": "Which scope the condition ends."
          },
          "tool_name": {
            "type": "string",
            "nullable": true,
            "description": "The resolved tool name a `has_tool_call` condition matches. Required for that type."
          },
          "max_generations": {
            "type": "integer",
            "nullable": true,
            "description": "Generations the continuation chain may reach before further resumptions stop with `chain_limit`. Required for `max_chain_generations`, and must be a positive integer."
          }
        }
      },
      "AgentStepRule": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "step"
        ],
        "description": "One per-step override. Steps not named by a rule use the agent's own `tool_choice` and `active_tool_ids`.",
        "properties": {
          "step": {
            "type": "integer",
            "description": "The 1-indexed step of the turn this rule applies to. The numbering spans a `requires_action` pause."
          },
          "tool_choice": {
            "description": "Tool choice for this step — `auto`, `required`, `null`, or `{ \"type\": \"tool\", \"tool_name\": \"search\" }`. Stored verbatim."
          },
          "active_tool_ids": {
            "x-soat-ref": "tools",
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "Tool IDs active on this step."
          }
        }
      },
      "AgentBoundaryPolicy": {
        "type": "object",
        "nullable": true,
        "additionalProperties": false,
        "required": [
          "statement"
        ],
        "description": "Restricts which SOAT actions the agent may invoke. Evaluated as the intersection with the caller's own policy, so it can only narrow.",
        "properties": {
          "statement": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentBoundaryPolicyStatement"
            },
            "description": "IAM policy statements, in the policy document grammar."
          }
        }
      },
      "AgentBoundaryPolicyStatement": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "effect",
          "action"
        ],
        "properties": {
          "effect": {
            "type": "string",
            "enum": [
              "Allow",
              "Deny"
            ],
            "example": "Allow"
          },
          "action": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "memories:*",
              "agents:DeleteAgent"
            ]
          },
          "resource": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "Resource SRN patterns. Omit to match every resource.",
            "example": [
              "srn:proj_abc:memories:*"
            ]
          },
          "condition": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Condition block, in the same grammar a policy document uses: keys are condition operators mapping to context-key/value maps.",
            "example": {
              "StringEquals": {
                "soat:ResourceTag/env": "prod"
              }
            }
          }
        }
      },
      "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`, `cache_write_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"
        ],
        "additionalProperties": false,
        "properties": {
          "prices": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "model",
                "component",
                "unit",
                "unit_price",
                "effective_from"
              ],
              "additionalProperties": false,
              "properties": {
                "model": {
                  "type": "string",
                  "example": "gpt-4o"
                },
                "component": {
                  "type": "string",
                  "description": "The token component this row prices (`input_tokens`, `output_tokens`, `cached_tokens`, `cache_write_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": [
          "data",
          "total",
          "limit",
          "offset"
        ],
        "properties": {
          "total": {
            "type": "integer",
            "description": "Models matching `q` across every page"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "data": {
            "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`\n",
                  "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.\n",
                  "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.\n",
                  "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_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who resolved the item; null while pending and on expiry."
          },
          "resolved_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "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": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who acted: `user` or `api_key`; 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"
          },
          "on_behalf_of": {
            "$ref": "#/components/schemas/OnBehalfOf"
          },
          "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",
        "additionalProperties": false,
        "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.\n",
            "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.\n",
            "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.\n",
            "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.\n",
            "example": "chat_V1StGXR8Z5jdHi6B"
          },
          "model": {
            "type": "string",
            "description": "Model identifier. Overrides the provider's `default_model`, or the chat's `model`, when specified.\n",
            "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.\n",
            "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.\n"
          }
        }
      },
      "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"
            }
          },
          "usage": {
            "type": "object",
            "description": "What the call reported, in OpenAI's field names rather than the `UsageTotals` every other surface uses: this endpoint exists so an OpenAI SDK can target SOAT by base URL alone, and that SDK reads these names. A dimension the provider did not report is 0.\n\nA completion is metered like any other call — see [usage](/docs/modules/usage). This is the provider's report, not the priced ledger line.",
            "properties": {
              "prompt_tokens": {
                "type": "integer",
                "description": "Full prompt tokens, cache reads included."
              },
              "completion_tokens": {
                "type": "integer"
              },
              "total_tokens": {
                "type": "integer"
              },
              "prompt_tokens_details": {
                "type": "object",
                "properties": {
                  "cached_tokens": {
                    "type": "integer"
                  }
                }
              },
              "completion_tokens_details": {
                "type": "object",
                "properties": {
                  "reasoning_tokens": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        }
      },
      "ExpectedVersion": {
        "type": "integer",
        "minimum": 1,
        "nullable": true,
        "description": "The version the caller believes the resource holds. When the resource is at any other version the write is refused with `409 VERSION_CONFLICT` and nothing is written; `meta.current_version` on that response names the version in force.\n\nOmit it to write unconditionally. Omitting it does not make the write unordered: two writes that reach the server together are still serialized, and the one whose version was taken first is refused the same way. What the field adds is refusing a write whose author read the resource some time ago and has not seen what happened since.\n\nThe `If-Match` header carries the same precondition for a client that prefers the HTTP spelling. Sending both with different versions is `400 VALIDATION_FAILED`.",
        "example": 3
      },
      "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"
          },
          "retrieval": {
            "type": "string",
            "enum": [
              "embed",
              "none",
              null
            ],
            "nullable": true,
            "description": "Whether this conversation's turns are embedded for vector retrieval. Turns are stored and chunked either way, so `none` leaves them readable and reachable by full-text search without paying for an embedding. `null` (the default) inherits the project's `default_conversation_retrieval`."
          }
        }
      },
      "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": {
            "description": "Optional structured metadata attached to the message",
            "example": {
              "phone": "5511999998888",
              "channel": "whatsapp"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          },
          "content": {
            "type": "string",
            "nullable": true,
            "description": "Full text content of the message"
          },
          "idempotency_key": {
            "type": "string",
            "nullable": true,
            "description": "The idempotency key the message was posted with, null when none was sent. A channel that posts each inbound message under its provider's message id finds the transcript row for that message by this key.",
            "example": "wamid.HBgLNTUxMTk4..."
          }
        }
      },
      "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.\n",
            "example": "Hello! How can I help you today?"
          },
          "message": {
            "description": "The stored assistant message, the record `GET /api/v1/conversations/{conversation_id}/messages` lists at its `position`. Its `document_id` addresses it.",
            "allOf": [
              {
                "$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.\n"
          },
          "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.",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "submit_tool_outputs"
                ]
              },
              "tool_calls": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "tool_name": {
                      "type": "string"
                    },
                    "args": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "GenerateConversationMessageResponse": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/GenerateConversationMessageCompleted"
          },
          {
            "$ref": "#/components/schemas/GenerateConversationMessageRequiresAction"
          }
        ],
        "discriminator": {
          "propertyName": "status",
          "mapping": {
            "completed": "#/components/schemas/GenerateConversationMessageCompleted",
            "requires_action": "#/components/schemas/GenerateConversationMessageRequiresAction"
          }
        }
      },
      "DeciderChoice": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "value",
          "description"
        ],
        "properties": {
          "value": {
            "type": "string",
            "description": "What a `choice` answer carries; unique within the question",
            "example": "billing"
          },
          "description": {
            "type": "string",
            "example": "Payments, invoices and refunds."
          }
        }
      },
      "DeciderLevel": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "label",
          "description"
        ],
        "properties": {
          "label": {
            "type": "string",
            "example": "Workaround available"
          },
          "description": {
            "type": "string",
            "example": "A task fails, but another way works."
          }
        }
      },
      "DeciderQuestion": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "name",
          "instructions"
        ],
        "description": "One question and its answer space. A `predicate` checks a condition and is answered with the probability it holds. A `choice` selects one of `choices` (2 to 20, in the order the model is shown them). A `score` rates against ordered `levels` (2 to 20); its answer is a level index, or a value between two when a tool weighs them.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "predicate",
              "choice",
              "score"
            ],
            "example": "choice"
          },
          "name": {
            "type": "string",
            "description": "Names the question's answer. Unique within the set; starts with a letter or underscore and holds only letters, digits and underscores (at most 64).",
            "example": "department"
          },
          "instructions": {
            "type": "string",
            "description": "What to judge",
            "example": "Which department should handle this complaint?"
          },
          "choices": {
            "type": "array",
            "description": "A `choice` question's options; required for `choice` only",
            "items": {
              "$ref": "#/components/schemas/DeciderChoice"
            }
          },
          "levels": {
            "type": "array",
            "description": "A `score` question's ordered levels; required for `score` only",
            "items": {
              "$ref": "#/components/schemas/DeciderLevel"
            }
          }
        }
      },
      "DeciderQuestions": {
        "type": "array",
        "minItems": 1,
        "maxItems": 20,
        "description": "The questions, in the order the model is shown them.",
        "items": {
          "$ref": "#/components/schemas/DeciderQuestion"
        },
        "example": [
          {
            "type": "choice",
            "name": "department",
            "instructions": "Which department should handle this complaint?",
            "choices": [
              {
                "value": "billing",
                "description": "Payments, invoices and refunds."
              },
              {
                "value": "technical",
                "description": "Problems using the product."
              }
            ]
          },
          {
            "type": "predicate",
            "name": "needs_human",
            "instructions": "Must a person read this before any automated reply?"
          }
        ]
      },
      "DecisionInput": {
        "description": "What is judged, not stored: a string; a list of `user` messages whose `content` is a string or `input_text` and `input_image` parts (an image as a base64 data URL); or any other JSON value, which an agent is shown as indented JSON. An array of objects carrying `role` is read as messages.",
        "example": "I was charged twice for the same order."
      },
      "Decider": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "dcd_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-soat-ref": "projects",
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "name": {
            "type": "string",
            "example": "support-triage"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "agent_id": {
            "x-soat-ref": "agents",
            "type": "string",
            "nullable": true,
            "description": "The tool-less agent that answers; null when a tool does",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "tool_id": {
            "x-soat-ref": "tools",
            "type": "string",
            "nullable": true,
            "description": "The http or pipeline tool that answers; null when an agent does",
            "example": "tool_V1StGXR8Z5jdHi6B"
          },
          "version": {
            "type": "integer",
            "description": "The question set's version. Bumped only by a write that changes `questions`.",
            "example": 1
          },
          "questions": {
            "$ref": "#/components/schemas/DeciderQuestions"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateDeciderRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Names exactly one of `agent_id` and `tool_id`.",
        "required": [
          "name",
          "questions"
        ],
        "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": "support-triage"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "agent_id": {
            "x-soat-ref": "agents",
            "type": "string",
            "description": "A tool-less agent that answers",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "tool_id": {
            "x-soat-ref": "tools",
            "type": "string",
            "description": "An http or pipeline tool that answers",
            "example": "tool_V1StGXR8Z5jdHi6B"
          },
          "questions": {
            "$ref": "#/components/schemas/DeciderQuestions"
          },
          "version_label": {
            "type": "string",
            "description": "Optional tag for version 1, e.g. `initial`",
            "example": "initial"
          }
        }
      },
      "UpdateDeciderRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "agent_id": {
            "x-soat-ref": "agents",
            "type": "string",
            "description": "Replaces the backend with this agent"
          },
          "tool_id": {
            "x-soat-ref": "tools",
            "type": "string",
            "description": "Replaces the backend with this tool"
          },
          "questions": {
            "$ref": "#/components/schemas/DeciderQuestions"
          },
          "version_label": {
            "type": "string",
            "description": "Optional tag for the version this write archives. Ignored when the write changes no question, since no version is archived.",
            "example": "add-account-option"
          },
          "expected_version": {
            "description": "Refuses the write unless the decider is at this version.",
            "allOf": [
              {
                "$ref": "#/components/schemas/ExpectedVersion"
              }
            ]
          }
        }
      },
      "DeciderVersion": {
        "type": "object",
        "description": "An immutable archive of a decider's question set at one version.",
        "properties": {
          "id": {
            "type": "string",
            "example": "dcd_ver_V1StGXR8Z5jdHi6B"
          },
          "decider_id": {
            "x-soat-ref": "deciders",
            "type": "string",
            "example": "dcd_V1StGXR8Z5jdHi6B"
          },
          "version": {
            "type": "integer",
            "example": 1
          },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "description": "The versioned surface as it stood at this version.",
            "properties": {
              "questions": {
                "$ref": "#/components/schemas/DeciderQuestions"
              }
            }
          },
          "label": {
            "type": "string",
            "nullable": true,
            "example": "restored from v2"
          },
          "created_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who produced this version. Null for a write with no principal behind it."
          },
          "created_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RestoreDeciderVersionRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "label": {
            "type": "string",
            "description": "Optional tag for the new version. Defaults to `restored from vN`.",
            "example": "rollback"
          }
        }
      },
      "CreateDecisionRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "input"
        ],
        "properties": {
          "input": {
            "$ref": "#/components/schemas/DecisionInput"
          },
          "metadata": {
            "description": "Caller-owned annotations stored on the decision and returned on every read — typically the id of what was judged, since the input itself is not stored. Written once, with the decision. A non-object is rejected with `400 VALIDATION_FAILED` and no decision is written.",
            "example": {
              "ticket_id": "ZD-48213"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          },
          "wait": {
            "type": "boolean",
            "default": false,
            "x-soat-tool-forced": true,
            "description": "True evaluates before answering and returns the settled decision. False — the default — returns the `queued` decision at once. A tool call, `builtin` or MCP, always waits.",
            "example": true
          }
        }
      },
      "CreateInlineDecisionRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Names exactly one of `agent_id` and `tool_id`.",
        "required": [
          "questions",
          "input"
        ],
        "properties": {
          "project_id": {
            "x-soat-ref": "projects",
            "type": "string",
            "description": "Project ID (required if not using project key auth)",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "agent_id": {
            "x-soat-ref": "agents",
            "type": "string",
            "description": "A tool-less agent that answers",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "tool_id": {
            "x-soat-ref": "tools",
            "type": "string",
            "description": "An http or pipeline tool that answers",
            "example": "tool_V1StGXR8Z5jdHi6B"
          },
          "questions": {
            "$ref": "#/components/schemas/DeciderQuestions"
          },
          "input": {
            "$ref": "#/components/schemas/DecisionInput"
          },
          "metadata": {
            "description": "Caller-owned annotations stored on the decision. A non-object is rejected with `400 VALIDATION_FAILED`.",
            "example": {
              "ticket_id": "ZD-48213"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          },
          "wait": {
            "type": "boolean",
            "default": false,
            "x-soat-tool-forced": true,
            "description": "True evaluates before answering and returns the settled decision. False — the default — returns the `queued` decision at once. A tool call, `builtin` or MCP, always waits.",
            "example": true
          }
        }
      },
      "AnswerProbability": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "value": {
            "description": "The choice's `value`, or the level's index",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              }
            ],
            "example": "billing"
          },
          "label": {
            "type": "string",
            "description": "The level's `label`; on a `score` answer only"
          },
          "probability": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "example": 0.95
          }
        }
      },
      "DecisionAnswer": {
        "type": "object",
        "description": "One question's answer, named after it. A `predicate` carries the `probability` its condition holds — 1 or 0 from an agent, which states no calibrated probability. A `choice` carries the chosen `choice`. A `score` carries the level index, or from a tool a probability-weighted value between levels. `probabilities` and `confidence` are present only when a tool backend supplied them, carried as supplied; SOAT does not vouch for their meaning.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "predicate",
              "choice",
              "score"
            ]
          },
          "name": {
            "type": "string",
            "example": "department"
          },
          "probability": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "example": 0.92
          },
          "choice": {
            "type": "string",
            "example": "billing"
          },
          "score": {
            "type": "number",
            "example": 1.1
          },
          "probabilities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnswerProbability"
            }
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "example": 0.93
          }
        }
      },
      "Decision": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "dec_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-soat-ref": "projects",
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "decider_id": {
            "x-soat-ref": "deciders",
            "type": "string",
            "nullable": true,
            "description": "The decider asked; kept after the decider is deleted. Null for a decision sent with its own questions.",
            "example": "dcd_V1StGXR8Z5jdHi6B"
          },
          "decider_version": {
            "type": "integer",
            "nullable": true,
            "description": "The question-set version the decision was answered under",
            "example": 1
          },
          "questions": {
            "type": "array",
            "nullable": true,
            "description": "The questions a decision sent with its own was answered under. Null for a decider's decision, whose questions are its `decider_version`.",
            "items": {
              "$ref": "#/components/schemas/DeciderQuestion"
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "completed",
              "failed"
            ]
          },
          "answers": {
            "type": "array",
            "nullable": true,
            "description": "One answer per question, in question order. Null until the decision completes, then never rewritten.",
            "items": {
              "$ref": "#/components/schemas/DecisionAnswer"
            }
          },
          "answers_by_name": {
            "type": "object",
            "nullable": true,
            "description": "The same answers keyed by question name, so a JSON Logic path reads one by name (`answers_by_name.department.choice`) rather than by position.",
            "additionalProperties": {
              "$ref": "#/components/schemas/DecisionAnswer"
            }
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why a `failed` decision failed."
          },
          "generation_id": {
            "x-soat-ref": "generations",
            "type": "string",
            "nullable": true,
            "description": "The generation that answered, once one has; null for a tool backend. Its receipt carries what the decision cost.",
            "example": "gen_V1StGXR8Z5jdHi6B"
          },
          "metadata": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DocumentVersion": {
        "type": "object",
        "description": "An immutable archive of a document's content and annotations at one version.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public ID of the archived version",
            "example": "doc_ver_V1StGXR8Z5jdHi6B"
          },
          "document_id": {
            "x-soat-ref": "documents",
            "type": "string",
            "description": "Public ID of the document this version belongs to",
            "example": "doc_V1StGXR8Z5jdHi6B"
          },
          "version": {
            "type": "integer",
            "description": "The archived version number",
            "example": 1
          },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "description": "The document's versioned surface as it stood at this version: its `content`, `title`, `path`, `metadata`, `tags` and chunk configuration. A full snapshot rather than a diff, because what a restore has to reproduce is what a run read — which is a read by version, and a diff chain would have to be replayed to answer it.\n\nA withdrawal is archived as a version too, carrying `withdrawn: true` and no content. Restoring one is refused; restore the version before it.\n\nDeliberately open rather than a fixed schema: an archive written by an earlier release of SOAT reflects the document surface **of its own time**, so it may carry fields the current API no longer documents."
          },
          "label": {
            "type": "string",
            "nullable": true,
            "description": "Optional human tag for this version, e.g. `pre-correction`. Set from the `version_label` of the write that archived it, or generated for a restore or a withdrawal.",
            "example": "pre-correction"
          },
          "created_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who produced this version. Null for a write with no principal behind it."
          },
          "created_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "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",
              "withdrawn"
            ],
            "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`); `withdrawn` — taken out of listings and search, with its chunks dropped, and restorable from an earlier version.",
            "example": "ready"
          },
          "version": {
            "type": "integer",
            "description": "The document's content version, starting at 1. Every write that changes the content or its annotations archives the state it replaced and increments this.",
            "example": 1
          },
          "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"
          }
        }
      },
      "DocumentRelation": {
        "type": "object",
        "description": "A typed edge one document asserts about another, within one project.",
        "properties": {
          "id": {
            "type": "string",
            "example": "doc_rel_V1StGXR8Z5jdHi6B"
          },
          "type": {
            "type": "string",
            "enum": [
              "derived_from",
              "supersedes",
              "cites"
            ],
            "description": "What the asserting document claims: it was `derived_from` the other, `supersedes` it, or `cites` it.",
            "example": "cites"
          },
          "from_document_id": {
            "type": "string",
            "description": "The document that asserts the edge",
            "example": "doc_V1StGXR8Z5jdHi6B"
          },
          "to_document_id": {
            "type": "string",
            "description": "The document the edge points at",
            "example": "doc_9Kp2mQxZ7bT4rN6W"
          },
          "created_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": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why the ingestion failed, when `status` is `failed`; its `code` is for example `FILE_PARSE_FAILED` or `INGESTION_TIMEOUT`.",
            "example": {
              "code": "INGESTION_TIMEOUT",
              "message": "Ingestion stalled and was marked failed."
            }
          }
        }
      },
      "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
              ]
            ]
          }
        }
      },
      "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."
              }
            }
          }
        }
      },
      "StoredError": {
        "type": "object",
        "description": "Why a resource's work failed, in the shape of an error response's `error` without `hint` and `docs_url`. Every `error` field a resource records carries it, so `code` can be read without first checking the type of `error`.",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "A key from the server's ERROR_CODES registry, documented on the Error Codes page. `INTERNAL_ERROR`, with the message `Internal Server Error`, records an unexpected failure whose detail reaches the server log only.",
            "example": "AI_PROVIDER_ERROR"
          },
          "message": {
            "type": "string",
            "example": "Provider returned 402: insufficient credits"
          },
          "meta": {
            "type": "object",
            "additionalProperties": true,
            "description": "The failure's own structured context, when it names any."
          }
        }
      },
      "ValidationProblem": {
        "type": "object",
        "description": "One reason a definition was refused. A validation refusal lists every one under `meta.problems`, and a dry-run validate endpoint answers the same items.",
        "required": [
          "code",
          "path",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "pattern": "^[A-Z][A-Z0-9_]*$",
            "description": "What kind of problem it is, stable across releases.",
            "example": "MISSING_FIELD"
          },
          "path": {
            "type": "string",
            "description": "Where in the request body the problem is.",
            "example": "nodes[0].agent_id"
          },
          "message": {
            "type": "string",
            "description": "The problem in words, naming the values involved.",
            "example": "agent node 'a' is missing required field 'agent_id'."
          }
        }
      },
      "DatasetItemInput": {
        "type": "array",
        "description": "Messages replayed verbatim as the generation's input",
        "items": {
          "type": "object",
          "required": [
            "role",
            "content"
          ],
          "additionalProperties": false,
          "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.\n\n`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.\n\n`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.\n\n`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.\n\n`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.\n\n`decider` grades each item with a decision of a project decider: its `score` expression reads the decision's `answers`. Like `tool`, it keys on its `name` and may appear several times.",
        "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"
            },
            {
              "$ref": "#/components/schemas/DeciderScorer"
            }
          ]
        }
      },
      "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
          }
        }
      },
      "DeciderScorer": {
        "type": "object",
        "description": "Grades each item with a decision of a project decider, under the decider version pinned when the run started (`decider_versions` on the run). The decision is requested as the run, with `metadata` naming the eval, run and dataset item. A failed decision, or a `score` that is not a number between 0 and 1, marks the **item** errored, never a score of 0.",
        "required": [
          "type",
          "name",
          "decider_id",
          "score",
          "pass_threshold"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "decider"
            ]
          },
          "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.",
            "example": "review"
          },
          "decider_id": {
            "x-soat-ref": "deciders",
            "type": "string",
            "description": "The decider that judges each item; it must belong to the eval's project.",
            "example": "dcd_V1StGXR8Z5jdHi6B"
          },
          "input": {
            "description": "JSON Logic mapping over `{ input, output, object, expected, item }` building the input the decider judges. Omitted, the input is that item context itself.",
            "example": {
              "customer": {
                "var": "input.0.content"
              },
              "reply": {
                "var": "output"
              }
            }
          },
          "score": {
            "description": "JSON Logic expression over `{ answers, answers_by_name }`, the decision's answers in question order and keyed by question name, that evaluates to the item's score in 0–1.",
            "example": {
              "var": "answers_by_name.resolves_issue.probability"
            }
          },
          "pass_threshold": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "The item passes this scorer when `score` is greater than or equal to this value. Required: a score read from a distribution says nothing about where \"good enough\" is.",
            "example": 0.7
          }
        }
      },
      "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"
          },
          "decision_id": {
            "type": "string",
            "description": "The decision that graded this entry; present for `decider` scorers",
            "example": "dec_V1StGXR8Z5jdHi6B"
          }
        }
      },
      "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": {
            "$ref": "#/components/schemas/MetadataBag"
          },
          "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
          },
          "group_by": {
            "type": "string",
            "nullable": true,
            "description": "The item `metadata` key a run rolls its scores up by; null when the eval groups nothing",
            "example": "kind"
          },
          "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"
          },
          "pass_rate_interval": {
            "$ref": "#/components/schemas/PassRateInterval"
          },
          "scored_item_count": {
            "type": "integer",
            "description": "Items that produced a score — errored items are excluded"
          },
          "baseline": {
            "$ref": "#/components/schemas/BaselineComparison"
          },
          "grouping": {
            "$ref": "#/components/schemas/RunGrouping"
          }
        }
      },
      "ScoreRollup": {
        "type": "object",
        "description": "One group's figures, computed as the run-level ones are",
        "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 in the group"
          },
          "pass_rate_interval": {
            "$ref": "#/components/schemas/PassRateInterval"
          },
          "scored_item_count": {
            "type": "integer",
            "description": "Non-errored items in the group"
          }
        }
      },
      "PassRateInterval": {
        "type": "object",
        "nullable": true,
        "description": "95% Wilson score interval around `pass_rate`: the range the pass rate of this agent over items like these plausibly spans, given how many items were scored. Null exactly when `pass_rate` is.",
        "properties": {
          "low": {
            "type": "number",
            "example": 0.63
          },
          "high": {
            "type": "number",
            "example": 0.84
          },
          "level": {
            "type": "number",
            "description": "The confidence level, always 0.95",
            "example": 0.95
          }
        }
      },
      "FlippedItems": {
        "type": "object",
        "description": "Compared items that changed verdict. `pass_rate_delta` is `(improved - regressed) / compared_item_count`.",
        "properties": {
          "improved": {
            "type": "integer",
            "description": "Passed in this run, failed in the baseline"
          },
          "regressed": {
            "type": "integer",
            "description": "Failed in this run, passed in the baseline"
          }
        }
      },
      "RunGrouping": {
        "type": "object",
        "description": "Scores rolled up per value of the eval's `group_by` key; absent when the eval declares none. An item belongs to the group its `metadata` names under that key, read when the run settles; only a string names a group. Errored items count in no group.",
        "properties": {
          "group_by": {
            "type": "string",
            "description": "The metadata key the groups were read from",
            "example": "kind"
          },
          "groups": {
            "type": "object",
            "description": "Group value → that group's figures",
            "additionalProperties": {
              "$ref": "#/components/schemas/ScoreRollup"
            }
          },
          "ungrouped_item_count": {
            "type": "integer",
            "description": "Scored items whose `metadata` holds no string under `group_by`, or whose dataset item was deleted"
          }
        }
      },
      "GroupComparison": {
        "type": "object",
        "description": "One group's baseline comparison, computed as the run-level one is",
        "properties": {
          "compared_item_count": {
            "type": "integer"
          },
          "added_item_count": {
            "type": "integer"
          },
          "removed_item_count": {
            "type": "integer"
          },
          "pass_rate_delta": {
            "type": "number",
            "nullable": true
          },
          "flipped": {
            "$ref": "#/components/schemas/FlippedItems"
          },
          "p_value": {
            "type": "number",
            "nullable": true
          },
          "scorers": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "mean_delta": {
                  "type": "number"
                },
                "pass_rate_delta": {
                  "type": "number"
                }
              }
            }
          }
        }
      },
      "BaselineGrouping": {
        "type": "object",
        "description": "The baseline comparison per group; absent when the eval declares no `group_by`. Both runs' items are grouped by the same labels, so an item relabelled between the runs is compared within one group.",
        "properties": {
          "group_by": {
            "type": "string",
            "example": "kind"
          },
          "groups": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/GroupComparison"
            }
          },
          "ungrouped_item_count": {
            "type": "integer",
            "description": "Items scorable in both runs that name no group"
          }
        }
      },
      "BaselineComparison": {
        "type": "object",
        "nullable": true,
        "description": "Comparison against the run named by `baseline_run_id`; absent when the run named none.\n\nEvery 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"
          },
          "flipped": {
            "$ref": "#/components/schemas/FlippedItems"
          },
          "p_value": {
            "type": "number",
            "nullable": true,
            "description": "Exact McNemar p-value of the `flipped` split: the probability of a split at least this uneven were an item equally likely to flip either way, which is what an unchanged agent produces. `1` when no item flipped; null when the runs share no comparable item.",
            "example": 0.03
          },
          "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"
                }
              }
            }
          },
          "grouping": {
            "$ref": "#/components/schemas/BaselineGrouping"
          }
        }
      },
      "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
          },
          "decider_versions": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "integer"
            },
            "description": "The decider version each `decider` scorer grades under, keyed by scorer name and resolved when the run started. Null when the eval has no decider scorer.",
            "example": {
              "review": 2
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "completed",
              "failed",
              "cancelled"
            ]
          },
          "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": {
            "description": "The caller-owned key/value metadata supplied when the run was started, returned verbatim. Absent 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"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          },
          "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": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Item-level failure reason. A generation that did not complete — a `requires_action` pause (`GENERATION_NOT_COMPLETED`), a provider failure — or a scorer that could not grade the output 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_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who acknowledged the exception; null until acknowledged."
          },
          "acknowledged_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "resolved_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who resolved the exception; null until resolved."
          },
          "resolved_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "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"
        ],
        "additionalProperties": false,
        "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": {
            "$ref": "#/components/schemas/MetadataBag"
          }
        }
      },
      "PresignedUrlRequest": {
        "type": "object",
        "required": [
          "project_id"
        ],
        "additionalProperties": false,
        "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",
        "additionalProperties": false,
        "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": {
            "$ref": "#/components/schemas/MetadataBag"
          }
        }
      },
      "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": {
            "$ref": "#/components/schemas/MetadataBag"
          },
          "tags": {
            "$ref": "#/components/schemas/TagBag"
          },
          "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.\n",
        "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.\n",
            "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. `{ \"ref_attr\": \"LogicalId.attribute\" }` resolves a named attribute of a created resource, except one carrying credential material — a trigger's or webhook's `secret` is refused with 400 VALIDATION_FAILED, since a formation is readable by anyone holding `formations:GetFormation`. Read those from the resource's own secret route instead.\n",
            "nullable": true
          },
          "metadata": {
            "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.\n",
            "allOf": [
              {
                "$ref": "#/components/schemas/NullableMetadataBag"
              }
            ]
          }
        }
      },
      "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.\n",
            "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.\n",
            "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",
              "additionalProperties": false,
              "properties": {
                "tool_id": {
                  "x-soat-ref": "tools",
                  "type": "string",
                  "description": "Public ID of the tool to attach."
                },
                "tool": {
                  "type": "object",
                  "nullable": true,
                  "description": "Inline tool definition. Not supported in a template: declare a tool resource and reference it via `tool_id`."
                }
              }
            },
            "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": {
            "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",
              "additionalProperties": false,
              "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",
              "additionalProperties": false,
              "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.",
            "additionalProperties": false,
            "properties": {
              "statement": {
                "type": "array",
                "description": "List of IAM policy statements",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "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)"
                    },
                    "condition": {
                      "type": "object",
                      "nullable": true,
                      "additionalProperties": true,
                      "description": "Condition block, in the same grammar a policy document uses: keys are condition operators mapping to context-key/value maps."
                    }
                  }
                }
              }
            }
          },
          "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."
          },
          "max_output_tokens": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Maximum number of tokens the model may generate per step of a turn. A response that reaches it stops with `stop_reason` `length`. Null leaves the model's default.",
            "example": 1024
          },
          "reasoning": {
            "type": "object",
            "nullable": true,
            "additionalProperties": false,
            "required": [
              "effort"
            ],
            "description": "How much the model reasons before answering. Translated per provider (Anthropic thinking, Gemini thinking config, OpenAI reasoning effort) against what the model supports; a model that cannot reason ignores it. `none` turns reasoning off where the model allows it. Reasoning tokens are metered as output. Null leaves the model's default.",
            "properties": {
              "effort": {
                "type": "string",
                "enum": [
                  "none",
                  "low",
                  "medium",
                  "high"
                ],
                "example": "low"
              }
            }
          },
          "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 memories are injected into every generation.",
            "additionalProperties": false,
            "properties": {
              "memory_store_ids": {
                "x-soat-ref": "memory-stores",
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Public IDs of memory stores to retrieve from"
              },
              "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"
              },
              "tags": {
                "description": "Retrieve from documents and memories whose tags contain all these key-value pairs.",
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TagBag"
                  }
                ]
              },
              "min_score": {
                "type": "number",
                "description": "Minimum raw cosine similarity (0–1) a vector candidate must reach to be ranked. Omitted, there is no floor."
              },
              "rrf_k": {
                "type": "integer",
                "description": "The `k` in the fusion term `1 / (k + rank)`; smaller weights the top of each ranking more heavily"
              },
              "recency_half_life_days": {
                "type": "number",
                "description": "Half-life in days of the decay applied to memory results after fusion; `0` disables it"
              },
              "limit": {
                "type": "integer",
                "description": "Maximum number of chunks to inject"
              },
              "write_memory_store_id": {
                "x-soat-ref": "memory-stores",
                "type": "string",
                "nullable": true,
                "description": "Public ID of the memory store the agent can write to. When set, a `write_memory` tool is automatically available to the agent."
              }
            }
          },
          "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`."
          },
          "prompt_caching": {
            "type": "object",
            "nullable": true,
            "description": "Prompt caching for this agent's turns. `{\"enabled\": true}` marks a cache breakpoint at the end of the system block and on the last message of each step. Null or omitted is off.",
            "additionalProperties": false,
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Whether the breakpoint is marked. Defaults to false."
              }
            }
          }
        }
      },
      "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.",
            "additionalProperties": false,
            "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`."
              },
              "response_mode": {
                "type": "string",
                "nullable": true,
                "description": "How a 2xx response body is returned: `json` (default) or `base64`, which returns `{ content_type, filename, data_base64 }` for binary targets."
              },
              "auth": {
                "type": "object",
                "nullable": true,
                "additionalProperties": false,
                "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.",
                "properties": {
                  "type": {
                    "type": "string",
                    "description": "`aws_sigv4` or `gcp_service_account`"
                  },
                  "region": {
                    "type": "string",
                    "nullable": true,
                    "description": "`aws_sigv4`: the signing region"
                  },
                  "service": {
                    "type": "string",
                    "nullable": true,
                    "description": "`aws_sigv4`: the signing service"
                  },
                  "access_key_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "`aws_sigv4`: the access key id"
                  },
                  "secret_access_key": {
                    "type": "string",
                    "nullable": true,
                    "description": "`aws_sigv4`: the secret access key"
                  },
                  "session_token": {
                    "type": "string",
                    "nullable": true,
                    "description": "`aws_sigv4`: the temporary credential's session token"
                  },
                  "credentials": {
                    "type": "string",
                    "nullable": true,
                    "description": "`gcp_service_account`: the service account key file JSON, as a string"
                  },
                  "scopes": {
                    "type": "array",
                    "nullable": true,
                    "items": {
                      "type": "string"
                    },
                    "description": "`gcp_service_account`: the scopes to mint for"
                  }
                }
              }
            }
          },
          "mcp": {
            "type": "object",
            "nullable": true,
            "description": "MCP server connection configuration. Required for `mcp` tools.",
            "additionalProperties": false,
            "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": {
            "description": "Free-form tags on the case, e.g. `{\"topic\": \"billing\"}`",
            "allOf": [
              {
                "$ref": "#/components/schemas/NullableMetadataBag"
              }
            ]
          }
        }
      },
      "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`, `tool`, or `decider`. 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."
          },
          "group_by": {
            "type": "string",
            "nullable": true,
            "minLength": 1,
            "maxLength": 255,
            "description": "A key of the items' `metadata`; a run rolls its scores up per string value of it. Omit for no grouping."
          }
        }
      },
      "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": {
            "description": "Arbitrary metadata key-value pairs",
            "allOf": [
              {
                "$ref": "#/components/schemas/NullableMetadataBag"
              }
            ]
          },
          "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."
          }
        }
      },
      "MemoryStoreResourceProperties": {
        "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 store display name"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "What this memory store stores"
          },
          "tags": {
            "$ref": "#/components/schemas/NullableTagBag"
          },
          "duplicate_threshold": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "maximum": 1,
            "description": "Cosine similarity at or above which an incoming fact is already known and the write is skipped. Null uses the algorithm constant (0.95). This is where a template sets the corpus's dedup policy: a `memory` resource has no threshold of its own."
          },
          "supersede_threshold": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "maximum": 1,
            "description": "Cosine similarity at or above which an incoming fact restates a known one that has changed, retiring it. Null uses the algorithm constant (0.90). Must be lower than the effective `duplicate_threshold`."
          }
        }
      },
      "MemoryResourceProperties": {
        "description": "Adds a single memory to a memory store.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "memory_store_id",
          "content"
        ],
        "properties": {
          "memory_store_id": {
            "x-soat-ref": "memory-stores",
            "type": "string",
            "description": "Public ID of the parent memory store (or ref expression)"
          },
          "content": {
            "type": "string",
            "description": "Text content of the memory"
          },
          "source_type": {
            "type": "string",
            "enum": [
              "manual",
              "conversation"
            ],
            "description": "Whether there is a source to point at (defaults to manual). `conversation` requires `source_id`."
          },
          "source_id": {
            "type": "string",
            "nullable": true,
            "description": "The conversation this fact was learned in, when `source_type` is `conversation`; null when it is `manual`."
          },
          "tags": {
            "description": "Per-memory key-value tags for memory-granularity filtering in knowledge search.",
            "allOf": [
              {
                "$ref": "#/components/schemas/NullableTagBag"
              }
            ]
          },
          "metadata": {
            "description": "Arbitrary structured metadata attached to the memory",
            "allOf": [
              {
                "$ref": "#/components/schemas/NullableMetadataBag"
              }
            ]
          }
        }
      },
      "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",
              "additionalProperties": false,
              "required": [
                "ai_provider_id",
                "model"
              ],
              "properties": {
                "ai_provider_id": {
                  "x-soat-ref": "ai-providers",
                  "type": "string",
                  "description": "AI provider in the route's project."
                },
                "model": {
                  "type": "string",
                  "description": "Model name to call on that provider."
                },
                "timeout_seconds": {
                  "type": "integer",
                  "nullable": true,
                  "description": "Per-attempt deadline. Omitted means no per-target deadline."
                },
                "max_retries": {
                  "type": "integer",
                  "nullable": true,
                  "description": "Retries on this target before falling through to the next one."
                }
              }
            }
          },
          "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. `documents.ingested`)"
          }
        }
      },
      "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"
          },
          "tool_context": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "string"
            },
            "description": "Caller context every firing forwards to the run it starts, so an agent whose tools authorize through `{{context:<key>}}` can be scheduled. Write-only. A value may be a `{{secret:sec_...}}` reference, which is what a template should carry — the credential stays in the secret store and only its id is checked in (a `sub` such as `{{secret:${MySecret}}}` for a template secret); a secret's name does not resolve and is refused"
          },
          "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": {
            "description": "Caller-owned annotations on the file, stored as the object they were written as; `null` clears the bag. Keys are stored and returned verbatim in the casing supplied.",
            "allOf": [
              {
                "$ref": "#/components/schemas/NullableMetadataBag"
              }
            ]
          }
        }
      },
      "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": false,
            "description": "Policy document containing an array of statements",
            "properties": {
              "statement": {
                "type": "array",
                "description": "IAM policy statements.",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "effect",
                    "action"
                  ],
                  "properties": {
                    "effect": {
                      "type": "string",
                      "description": "`Allow` or `Deny`"
                    },
                    "action": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "IAM action strings, e.g. `memories:*`."
                    },
                    "resource": {
                      "type": "array",
                      "nullable": true,
                      "items": {
                        "type": "string"
                      },
                      "description": "Resource SRN patterns. Omit to match every resource."
                    },
                    "condition": {
                      "type": "object",
                      "nullable": true,
                      "additionalProperties": true,
                      "description": "Condition block: keys are condition operators mapping to context-key/value maps."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "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, cache_write_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."
          },
          "max_context_messages": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Maximum number of recent conversation messages sent to the model on each generation, taking precedence over the agent's. null defers to the agent."
          },
          "tool_context": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "nullable": true,
            "description": "Optional context passed to tool calls, a bag of string values; a non-string value is refused. Write-only: no read of a session returns it."
          }
        }
      },
      "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": {
            "description": "Arbitrary JSON metadata",
            "allOf": [
              {
                "$ref": "#/components/schemas/NullableMetadataBag"
              }
            ]
          }
        }
      },
      "MemoryRuleResourceProperties": {
        "description": "Declares what a memory store accepts from completed agent turns: a selector, an event, and a handler. With neither `agent_id` nor `tool_id` the built-in extractor runs, configurable through `prompt`, `ai_provider_id` and `model`. See the Memory Rules section of the Memories module docs.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "memory_store_id",
          "on"
        ],
        "properties": {
          "memory_store_id": {
            "x-soat-ref": "memory-stores",
            "type": "string",
            "description": "Public ID of the destination memory store"
          },
          "on": {
            "type": "string",
            "description": "`generations.completed` (once per completed turn, and the only event the built-in extractor may bind to) or `conversation_messages.generated` (per persisted assistant reply, custom handlers only)."
          },
          "source_agent_ids": {
            "x-soat-ref": "agents",
            "type": "array",
            "nullable": true,
            "description": "Agents whose turns this rule reads; null is every agent in the project",
            "items": {
              "type": "string"
            }
          },
          "agent_id": {
            "x-soat-ref": "agents",
            "type": "string",
            "nullable": true,
            "description": "Handler agent ID (mutually exclusive with tool_id)"
          },
          "tool_id": {
            "x-soat-ref": "tools",
            "type": "string",
            "nullable": true,
            "description": "Handler tool ID (mutually exclusive with agent_id)"
          },
          "action": {
            "type": "string",
            "nullable": true,
            "description": "Operation id, for a tool handler"
          },
          "preset_parameters": {
            "type": "object",
            "nullable": true,
            "description": "Merged into a tool handler's input before invocation"
          },
          "prompt": {
            "type": "string",
            "nullable": true,
            "description": "Replaces the built-in extractor's task instructions (no handler only)"
          },
          "ai_provider_id": {
            "x-soat-ref": "ai-providers",
            "type": "string",
            "nullable": true,
            "description": "Provider override for the built-in extractor (no handler only)"
          },
          "model": {
            "type": "string",
            "nullable": true,
            "description": "Model override for the built-in extractor (no handler only)"
          },
          "enabled": {
            "type": "boolean",
            "description": "A disabled rule is kept and never fires"
          }
        }
      },
      "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_store_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_store_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"
          },
          "output_mapping": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Optional shape of a completed run's `output`: each key an output field, each value JSON Logic over `{ \"state\": <final run state> }`. Omitted keys `output` by terminal node id."
          }
        }
      },
      "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"
          }
        }
      },
      "MetadataSchemaResourceProperties": {
        "description": "Declares what `metadata` must satisfy for one resource type under one selector, so a template ships the corpus and the rule governing it together. `resource_type` is immutable: changing it in a template is a delete and a create.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "resource_type",
          "schema"
        ],
        "properties": {
          "resource_type": {
            "type": "string",
            "enum": [
              "document"
            ],
            "description": "The resource whose metadata this declaration governs."
          },
          "path_prefix": {
            "type": "string",
            "description": "The selector a `document` declaration must carry: the directory it governs, matched on a path boundary."
          },
          "schema": {
            "type": "object",
            "description": "A JSON Schema, stored as written."
          }
        }
      },
      "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`, `window`, and `meter_type` 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",
              "storage_bytes"
            ],
            "description": "The metric being capped"
          },
          "window": {
            "type": "string",
            "enum": [
              "rolling_1m",
              "rolling_1h",
              "rolling_24h",
              "calendar_month",
              "current"
            ],
            "description": "The window over which the metric is aggregated. storage_bytes caps a stored total rather than a windowed one, so it takes current and refuses every other value; current is refused for every other metric."
          },
          "limit": {
            "type": "number",
            "description": "The cap. Positive integer for requests/tokens/storage_bytes (bytes); 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."
          },
          "meter_type": {
            "type": "string",
            "enum": [
              "llm_tokens",
              "compute_execution",
              "api_request",
              "storage",
              "tool_execution"
            ],
            "description": "Only for metric cost_usd. The meter this cap answers for; omit it and the cap sums every priced meter. 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."
          }
        }
      },
      "DeciderResourceProperties": {
        "description": "Creates a decider — a versioned question set answered against a caller's input. Names exactly one backend: `agent_id` (a tool-less agent) or `tool_id` (an `http` or `pipeline` tool), either a `{ \"ref\": … }` to a resource in the same template. Only a change to `questions` archives a new version. Mirrors the deciders REST contract.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "questions"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Human-readable name, unique per project"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Optional description"
          },
          "agent_id": {
            "x-soat-ref": "agents",
            "type": "string",
            "description": "The tool-less agent that answers"
          },
          "tool_id": {
            "x-soat-ref": "tools",
            "type": "string",
            "description": "The http or pipeline tool that answers"
          },
          "questions": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "The questions (`type`, `name`, `instructions`, `choices` or `levels`), 1 to 20 of them, validated as the deciders REST contract validates them."
          }
        }
      },
      "PlaybookResourceProperties": {
        "description": "Creates a playbook — a versioned template of nodes that cases are filled against. An executor's `id` and a transformation's `tool` accept a `{ \"ref\": … }` to a resource in the same template. The version a deploy writes runs as the credential that deployed it, checked as a direct write is: `playbooks:CreatePlaybook` plus the start action of every executor it names. Only a change to `definition` archives a new version; a playbook with cases is not deleted. Mirrors the playbooks REST contract.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "definition"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Human-readable name, unique per project"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "What the playbook is for"
          },
          "definition": {
            "description": "The nodes and how their inputs are sourced",
            "allOf": [
              {
                "$ref": "#/components/schemas/PlaybookDefinition"
              }
            ]
          }
        }
      },
      "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`, `decider`, `document`, `file`, `guardrail`, `ingestion_rule`, `memory_store`, `memory`, `memory_rule`, `metadata_schema`, `model_route`, `eval`, `orchestration`, `playbook`, `policy`, `project_price`, `quota`, `secret`, `session`, `webhook`, `trigger` and `workflow`.\n\nThis 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.\n"
          },
          "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.\n"
          },
          "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.\n"
          },
          "metadata": {
            "$ref": "#/components/schemas/NullableMetadataBag"
          }
        }
      },
      "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_store`)"
          },
          "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": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FormationTemplate"
              }
            ],
            "description": "The template as deployed. Credential-bearing properties — a `secret` resource's `value`, and any property a custom resource type declares `write_only` — read back as `{ \"no_echo\": true }` rather than the value that was supplied.\n"
          },
          "outputs": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "nullable": true,
            "description": "Resolved output values after stack deployment. An output that resolved a credential attribute on a formation deployed before those were refused is omitted.\n"
          },
          "status": {
            "type": "string",
            "enum": [
              "creating",
              "active",
              "updating",
              "failed",
              "deleting",
              "deleted",
              "delete_failed"
            ],
            "description": "Formation status"
          },
          "metadata": {
            "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`).\n",
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          },
          "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.\n"
          },
          "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.\n"
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why the formation is `failed` or `delete_failed`. 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`. A failed apply names the resource that broke it in `meta` (`logical_id`, `resource_type`); a failed teardown lists every blocker under `meta.failures`.\n\nOne 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.\n"
          },
          "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": "JSON path to the field with the error"
          },
          "message": {
            "type": "string",
            "description": "Error description"
          }
        }
      },
      "ValidationResult": {
        "type": "object",
        "properties": {
          "valid": {
            "type": "boolean"
          },
          "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"
          }
        }
      },
      "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": [
              "completed",
              "failed"
            ]
          },
          "physical_resource_id": {
            "type": "string",
            "nullable": true
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why the resource's step failed, when `status` is `failed`."
          }
        }
      },
      "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": [
              "queued",
              "running",
              "completed",
              "failed"
            ]
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FormationEvent"
            },
            "nullable": true
          },
          "plan": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanResult"
              }
            ],
            "nullable": true
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why this operation failed. Null for a completed 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 started this one: a sub-agent invocation, an approval continuation, a client-tool re-handoff or a memory-rule handler turn. Null for top-level generations.\n"
          },
          "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.\n"
          },
          "conversation_id": {
            "x-soat-ref": "conversations",
            "type": "string",
            "nullable": true,
            "description": "Public ID of the conversation this turn served, or null for a generation started outside one — a direct call, a trigger, an orchestration node. This is the edge a memory assertion walks up to reach the conversation a fact was learned in.\n"
          },
          "session_id": {
            "x-soat-ref": "sessions",
            "type": "string",
            "nullable": true,
            "description": "Public ID of the session this generation was dispatched through, or null for a generation started outside one. This is the link a per-session cost reading follows; the session's own `usage` field reports the roll-up directly.\n"
          },
          "actor_id": {
            "x-soat-ref": "actors",
            "type": "string",
            "nullable": true,
            "description": "Public ID of the end-user actor the generation was attributed to, or null when none was. Copied onto the generation's usage event, so it is the same dimension `group_by=actor` buckets on.\n"
          },
          "started_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who started the generation"
          },
          "started_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "status": {
            "type": "string",
            "description": "Lifecycle status of the generation",
            "enum": [
              "running",
              "requires_action",
              "completed",
              "failed"
            ],
            "example": "failed"
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the generation reached a terminal state"
          },
          "last_activity_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "stop_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why the generation stopped. Either the model provider's own finish reason relayed unchanged ('stop', 'tool-calls', 'length', …) or one the platform names itself: 'max_steps' when the turn spent its whole step budget on tool calls, 'depth_guard' when a nested call exceeded the call depth, 'chain_limit' when a continuation chain reached its generation budget, or 'error' when the turn failed.\n",
            "example": "error"
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why the generation failed: `AI_PROVIDER_ERROR` for an upstream provider failure, `GENERATION_FAILED` for any other failure of the turn."
          },
          "action_id": {
            "type": "string",
            "nullable": true,
            "description": "Logical action label supplied on the generate request. Recorded on the generation's usage event for per-action spend rollups.\n"
          },
          "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.\n"
          },
          "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.\n"
          },
          "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.\n"
          },
          "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)).\n"
          },
          "extraction": {
            "type": "object",
            "nullable": true,
            "description": "What each [memory rule](/docs/modules/memories#memory-rules) bound to `generations.completed` wrote for this turn, keyed by the rule's id — a store may have several rules, and one flat pair of counts could not say which produced them. Absent when no rule fired. Rules bound to `conversation_messages.generated` are recorded only as memory assertions. The rows behind every count are listed by `GET /api/v1/generations/{generation_id}/memory-assertions`, so the summary and what it summarizes can be reconciled.\n",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "candidates": {
                  "type": "integer",
                  "description": "Number of candidates the rule's handler proposed"
                },
                "created": {
                  "type": "integer",
                  "description": "Number of new memories created"
                },
                "superseded": {
                  "type": "integer",
                  "description": "Number of candidates that restated a known fact that had changed, retiring the memory holding it\n"
                },
                "skipped": {
                  "type": "integer",
                  "description": "Number of candidates skipped (e.g. duplicates)"
                }
              }
            }
          },
          "tool_surface": {
            "type": "object",
            "nullable": true,
            "description": "What this turn's tool definitions cost to send — the one part of a prompt no provider reports and no caller can derive, since the tool block is a constant inside the reported totals and identical on every step.\n\nNull when the surface was never measured: a generation from before the field, or a path that resolves none. That is **not** what `tools: 0` means, which is a measured agent with nothing bound.\n\nNot a meter. `estimated_tokens` is an estimate and is never priced — `usage` and the usage events are the billing record.",
            "properties": {
              "tools": {
                "type": "integer",
                "description": "Tools resolved for the turn, before any per-step narrowing by `step_rules`.",
                "example": 61
              },
              "bytes": {
                "type": "integer",
                "description": "Serialized length of those definitions — name, description and input schema — in canonical JSON, not the provider's wire format. Exact, and comparable across providers, which is what makes two agents' surfaces worth comparing.",
                "example": 152161
              },
              "estimated_tokens": {
                "type": "integer",
                "description": "`bytes` over a measured bytes-per-token ratio. An estimate, and named one.",
                "example": 45148
              }
            }
          },
          "retrieval": {
            "type": "array",
            "nullable": true,
            "description": "What `knowledge_config` retrieval injected into this turn, in the order it was injected: which document, at which version, and which chunk, or which memory. Written by the server at turn start and never writable; pointers only, never the text.\n\nNull when no retrieval ran — the agent has no `knowledge_config`, or the turn had neither a query nor a filter to search with. An empty array is a retrieval that ran and matched nothing.\n\nNot content: a content purge leaves it standing, and zero retention still writes it. `document_version` names an archived version, so the text the turn read stays readable after the document changes.",
            "items": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/GenerationRetrievedDocument"
                },
                {
                  "$ref": "#/components/schemas/GenerationRetrievedMemory"
                }
              ],
              "discriminator": {
                "propertyName": "source_type",
                "mapping": {
                  "document": "#/components/schemas/GenerationRetrievedDocument",
                  "memory": "#/components/schemas/GenerationRetrievedMemory"
                }
              }
            }
          },
          "idempotency_key": {
            "type": "string",
            "nullable": true,
            "description": "The deduplication key the generation was started under, unique within the project and claimed for as long as the generation record exists. Null for a generation started without one.",
            "example": "discord-1287654321098765432"
          },
          "usage": {
            "allOf": [
              {
                "$ref": "#/components/schemas/UsageTotals"
              }
            ],
            "nullable": true,
            "description": "What the turn cost: token counts and `cost_usd` for every event metered against this generation. Present on the single-generation read; omitted from the listing, where it would be a query per row.\n\nNull when nothing has been metered yet — a turn still in flight, or one whose metering failed. A turn is metered once at the end, so this is one event's figures in the ordinary case."
          },
          "routing": {
            "type": "object",
            "nullable": true,
            "description": "What the model route did for this generation. Present only when the agent resolves its model through a `model_route_id`.\n",
            "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.\n"
              },
              "fallbacks": {
                "type": "integer",
                "description": "How many times the route moved past a target during this generation (cumulative across a multi-step run).\n"
              },
              "attempts": {
                "type": "array",
                "description": "Every attempt, in order, across every LLM call of the run. An attempt with no `error_class` succeeded.\n",
                "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).\n"
                    }
                  }
                }
              }
            }
          },
          "metadata": {
            "description": "Caller-owned key/value annotations, attached at create time (via the `metadata` field on the create-agent-generation request) or afterwards (via the update-generation request), and returned verbatim. The server writes nothing here: every piece of state it owns — usage attribution, the served agent version, the route's record, the extraction summary, internal recovery state — is a field of its own, so no key written here can reach platform state. Keys are never transformed, and no key is reserved.\n",
            "example": {
              "team": "payments",
              "ticket_id": "OPS-4821"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          },
          "content_redacted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the generation's content was purged. Non-null means `metadata`, `error`, `extraction` and the internal recovery state have been cleared, while the usage/audit skeleton — ids, timestamps, status, stop reason and the attribution fields — is preserved.\n"
          },
          "content_redacted_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who purged the content: `user` or `api_key` for a purge request, `system` for the retention sweep and for content never stored under zero retention."
          },
          "content_redacted_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal — the API key's own id for key auth, so the record names which key acted. `retention_sweep` or `zero_retention` for `system`.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "GenerationRetrievedDocument": {
        "type": "object",
        "required": [
          "source_type",
          "document_id",
          "document_version",
          "chunk_id",
          "page",
          "similarity_score"
        ],
        "properties": {
          "source_type": {
            "type": "string",
            "enum": [
              "document"
            ],
            "example": "document"
          },
          "document_id": {
            "x-soat-ref": "documents",
            "type": "string",
            "description": "Public ID of the document the chunk belongs to",
            "example": "doc_V1StGXR8Z5jdHi6B"
          },
          "document_version": {
            "type": "integer",
            "nullable": true,
            "description": "The document version the chunk was read from. Read it back with the document's versions to see the exact text the turn was given.",
            "example": 3
          },
          "chunk_id": {
            "type": "string",
            "description": "Public ID of the injected chunk. Stops resolving once the document is re-chunked; `document_version` is the durable pointer.",
            "example": "dchunk_V1StGXR8Z5jdHi6B"
          },
          "page": {
            "type": "integer",
            "nullable": true,
            "description": "Page within the source PDF (1-indexed). Null for plain text.",
            "example": 2
          },
          "similarity_score": {
            "type": "number",
            "nullable": true,
            "description": "Raw cosine similarity (0–1) between the turn's query and the result. Null when the turn had no query, or the search answered from the lexical channel alone. The rank is the array order.",
            "example": 0.82
          }
        }
      },
      "GenerationRetrievedMemory": {
        "type": "object",
        "required": [
          "source_type",
          "memory_store_id",
          "memory_id",
          "similarity_score"
        ],
        "properties": {
          "source_type": {
            "type": "string",
            "enum": [
              "memory"
            ],
            "example": "memory"
          },
          "memory_store_id": {
            "x-soat-ref": "memory-stores",
            "type": "string",
            "description": "Public ID of the memory store the memory belongs to",
            "example": "mstore_V1StGXR8Z5jdHi6B"
          },
          "memory_id": {
            "type": "string",
            "description": "Public ID of the injected memory",
            "example": "mem_V1StGXR8Z5jdHi6B"
          },
          "similarity_score": {
            "type": "number",
            "nullable": true,
            "description": "Raw cosine similarity (0–1) between the turn's query and the result. Null when the turn had no query, or the search answered from the lexical channel alone. The rank is the array order.",
            "example": 0.82
          }
        }
      },
      "UpdateGenerationRequest": {
        "type": "object",
        "required": [
          "metadata"
        ],
        "additionalProperties": false,
        "properties": {
          "metadata": {
            "description": "Caller-supplied key/value metadata to shallow-merge into the generation record's caller-owned `metadata` bag. No key is reserved: server-owned state lives in its own top-level fields and cannot be written from here.\n",
            "example": {
              "team": "payments",
              "ticket_id": "OPS-4821"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          }
        }
      },
      "TranscriptToolCall": {
        "type": "object",
        "description": "One tool call the model made during a step.",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true,
            "description": "The call's ID, as the model provider issued it (e.g. `call_…`), used to correlate it with an entry in `tool_results`. Null when the stored step did not record one.\n"
          },
          "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.\n",
            "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.\n",
        "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.\n",
            "example": {
              "tempC": 18
            }
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "The tool's failure, when the step recorded one. A failure the tool raised with a code keeps it; any other is `TOOL_CALL_FAILED`, with the fields the thrown error carried (`name`, `status`, …) in `meta`.",
            "example": null
          }
        }
      },
      "TranscriptStep": {
        "type": "object",
        "description": "One model step. Projected from the stored step at read time — the stored shape is provider- and SDK-specific and is never put on the wire.\n",
        "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.\n",
            "example": 0
          },
          "text": {
            "type": "string",
            "description": "The text this step produced. Empty for a step that only called tools.\n",
            "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/UsageTotals"
              }
            ],
            "nullable": true,
            "description": "What this step reported, in the shape every usage surface uses. Null when the step recorded no usage at all; within a step a dimension the provider did not report reads 0. `cost_usd` is always null here — the ledger prices one event per generation, so a per-step price would disagree with it after any price change."
          }
        }
      },
      "GenerationTranscript": {
        "type": "object",
        "description": "One generation's turn, read back step by step. Assembled at read time from the generation record and the trace's steps object — never stored, so it dies with the content it projects.\n",
        "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.\n",
            "enum": [
              "running",
              "requires_action",
              "completed",
              "failed"
            ],
            "example": "completed"
          },
          "stop_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why the generation stopped — the provider's finish reason, or one of the platform's own ('max_steps', 'depth_guard', 'chain_limit', 'error').\n",
            "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.\n",
            "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.\n",
            "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.\n",
            "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.\n",
            "properties": {
              "content": {
                "type": "string",
                "nullable": true,
                "description": "The last step that produced text. Null for a turn that only called tools.\n",
                "example": "It's 18°C in Paris right now."
              },
              "finish_reason": {
                "type": "string",
                "nullable": true,
                "description": "The finish reason of the actual last step.",
                "example": "stop"
              }
            }
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why the generation failed."
          },
          "content_redacted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the generation's content was erased; null while it is intact.\n"
          },
          "content_redacted_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who erased the content."
          },
          "content_redacted_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal. `zero_retention` when the content was never stored, distinguishing it from content erased later.\n"
          }
        }
      },
      "GuardrailDocument": {
        "type": "object",
        "required": [
          "class"
        ],
        "additionalProperties": false,
        "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.\n",
        "properties": {
          "class": {
            "description": "A class literal (`A` / `B` / `C` / `D`) or a JSON Logic expression returning one. An invalid result resolves to `default_class`.\n",
            "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).\n"
          },
          "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\": [...] }`.\n"
          },
          "escalate": {
            "type": "boolean",
            "description": "When `true`, a failing guard routes to approval instead of tripping fail-closed.\n"
          },
          "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.\n"
          }
        }
      },
      "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.\n"
          },
          "context_mode": {
            "type": "string",
            "nullable": true,
            "enum": [
              "merge",
              "replace",
              null
            ],
            "description": "How tool-fetched context combines with the caller-supplied context.\n"
          },
          "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.\n",
        "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.\n"
          },
          "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.\n\nDeliberately 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_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who produced this version. Null for a write with no principal behind it."
          },
          "created_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RestoreGuardrailVersionRequest": {
        "type": "object",
        "additionalProperties": false,
        "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"
        ],
        "additionalProperties": false,
        "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",
        "additionalProperties": false,
        "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"
          },
          "expected_version": {
            "description": "Refuses the write unless the resource is at this version.",
            "allOf": [
              {
                "$ref": "#/components/schemas/ExpectedVersion"
              }
            ]
          }
        }
      },
      "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": {
            "$ref": "#/components/schemas/MetadataBag"
          },
          "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"
          },
          "document_version": {
            "type": "integer",
            "description": "The document version the chunk belongs to. Cite it to record which text was read; the document's versions keep that text after it changes.",
            "example": 3
          },
          "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": {
            "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.",
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          },
          "tags": {
            "$ref": "#/components/schemas/TagBag"
          },
          "content": {
            "type": "string",
            "nullable": true,
            "description": "Full text content of the document"
          },
          "score": {
            "type": "number",
            "description": "Reciprocal-rank-fusion relevance ranking — higher is better. The **ordering** it produces is the contract; the absolute value is not, is deliberately not rescaled into 0–1, and the formula behind it may change. Results are sorted by it. Nothing filters on it: `min_similarity` filters `similarity_score`. Only present when `query` was provided. Use `similarity_score` when you need the raw cosine value.",
            "example": 0.0328
          },
          "signals": {
            "type": "object",
            "description": "Which retrieval channels ranked this result before fusion, and its 1-based position in that channel's own ordering. A channel that did not return the result is absent, so presence reads as \"this channel found it\". `score` says where the result landed; this says how it got there — `{ \"lexical\": 1 }` is a pure token hit, `{ \"vector\": 2, \"lexical\": 1 }` a result both channels found and fusion promoted. Only present when `query` was provided; a channel that degraded is absent from every result. Diagnostic: nothing filters or sorts on it.",
            "properties": {
              "vector": {
                "type": "integer",
                "minimum": 1,
                "description": "Rank in the vector (cosine) channel, best first.",
                "example": 2
              },
              "lexical": {
                "type": "integer",
                "minimum": 1,
                "description": "Rank in the lexical (full-text) channel, best first.",
                "example": 1
              }
            },
            "example": {
              "vector": 2,
              "lexical": 1
            }
          },
          "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 — and populated on every result of a `query` search, a lexical-only hit included. Absent only when the embedding provider was unreachable and the search answered from the lexical channel alone, where there is no query vector to measure against.",
            "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",
          "memory_id",
          "memory_store_id",
          "memory_store_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"
          },
          "memory_id": {
            "type": "string",
            "description": "Public ID of the memory",
            "example": "mem_V1StGXR8Z5jdHi6B"
          },
          "memory_store_id": {
            "x-soat-ref": "memory-stores",
            "type": "string",
            "description": "Public ID of the parent memory store",
            "example": "mstore_V1StGXR8Z5jdHi6B"
          },
          "memory_store_name": {
            "type": "string",
            "description": "Human-readable name of the parent memory store",
            "example": "Customer Preferences"
          },
          "content": {
            "type": "string",
            "description": "Text content of the memory"
          },
          "tags": {
            "description": "The memory's tags; absent when it has none",
            "allOf": [
              {
                "$ref": "#/components/schemas/TagBag"
              }
            ]
          },
          "score": {
            "type": "number",
            "description": "Reciprocal-rank-fusion relevance ranking — higher is better. The **ordering** it produces is the contract; the absolute value is not, is deliberately not rescaled into 0–1, and the formula behind it may change. Results are sorted by it. Nothing filters on it: `min_similarity` filters `similarity_score`. Only present when `query` was provided. Use `similarity_score` when you need the raw cosine value.",
            "example": 0.0161
          },
          "signals": {
            "type": "object",
            "description": "Which retrieval channels ranked this result before fusion, and its 1-based position in that channel's own ordering. A channel that did not return the result is absent, so presence reads as \"this channel found it\". `score` says where the result landed; this says how it got there — `{ \"lexical\": 1 }` is a pure token hit, `{ \"vector\": 2, \"lexical\": 1 }` a result both channels found and fusion promoted. Only present when `query` was provided; a channel that degraded is absent from every result. Diagnostic: nothing filters or sorts on it.",
            "properties": {
              "vector": {
                "type": "integer",
                "minimum": 1,
                "description": "Rank in the vector (cosine) channel, best first.",
                "example": 2
              },
              "lexical": {
                "type": "integer",
                "minimum": 1,
                "description": "Rank in the lexical (full-text) channel, best first.",
                "example": 1
              }
            },
            "example": {
              "vector": 2,
              "lexical": 1
            }
          },
          "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 — and populated on every result of a `query` search, a lexical-only hit included. Absent only when the embedding provider was unreachable and the search answered from the lexical channel alone, where there is no query vector to measure against.",
            "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"
          },
          "memory_store_id": {
            "x-soat-ref": "memory-stores",
            "type": "string",
            "example": "mstore_V1StGXR8Z5jdHi6B"
          },
          "content": {
            "type": "string",
            "description": "The text this memory currently holds. Stored once per distinct text per store and shared with every assertion that stated it, so a memory carries no copy of its own.",
            "example": "The customer prefers email communication over phone calls"
          },
          "source_type": {
            "type": "string",
            "enum": [
              "manual",
              "conversation"
            ],
            "description": "Whether there is a source to point at. `conversation` means `source_id` names the conversation this fact was learned in; `manual` means there is nothing to point at — a direct API write, or an agent write made outside a conversation.",
            "example": "manual"
          },
          "source_id": {
            "type": "string",
            "nullable": true,
            "description": "The conversation this fact was learned in, when `source_type` is `conversation`; null when it is `manual`. Deliberately a loose pointer rather than a foreign key, so deleting the conversation leaves the id in place — the record of where the fact came from outlives its source.",
            "example": "conv_V1StGXR8Z5jdHi6B"
          },
          "tags": {
            "description": "Per-memory key-value tags, matched at memory granularity by knowledge search.",
            "allOf": [
              {
                "$ref": "#/components/schemas/TagBag"
              }
            ]
          },
          "metadata": {
            "description": "Arbitrary structured metadata attached to the memory",
            "example": {
              "evidence": "high"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          },
          "invalidated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the memory stopped holding, whether a supersede replaced it or a retraction withdrew it. Null means the memory is currently valid. Invalidated memories are excluded from listing, from write deduplication, and from knowledge search, but remain readable by ID — with their original text — for audit."
          },
          "superseded_by_memory_id": {
            "type": "string",
            "nullable": true,
            "description": "The memory that replaced this one, when it was superseded. Null for a valid memory and for a retracted one, which nothing replaced.",
            "example": "mem_V1StGXR8Z5jdHi6B"
          },
          "version": {
            "type": "integer",
            "description": "The memory's write version, starting at 1 and incremented on every update. States a precondition against it with `expected_version` or `If-Match` on `PUT /memories/{memory_id}`.",
            "example": 1
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MemoryWriteResult": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Memory"
          },
          {
            "type": "object",
            "properties": {
              "action": {
                "type": "string",
                "enum": [
                  "created",
                  "superseded",
                  "skipped"
                ],
                "description": "The outcome of the write. `created` means the fact was distinct and a new memory holds it. `superseded` means it restated a fact that had changed: the matched memory was invalidated, points at the replacement, and the replacement is what is returned. `skipped` means the fact was already known and the existing memory is returned unchanged. Every outcome is recorded as an assertion, readable at `GET /api/v1/memories/{memory_id}/assertions`."
              }
            }
          }
        ]
      },
      "MemoryAssertion": {
        "type": "object",
        "description": "One write attempt against a memory store: some principal, through some mechanism, claimed a fact. Append-only, and recorded whatever the outcome — including the skips, which left no record anywhere before.",
        "properties": {
          "id": {
            "type": "string",
            "example": "massert_V1StGXR8Z5jdHi6B"
          },
          "memory_store_id": {
            "x-soat-ref": "memory-stores",
            "type": "string",
            "example": "mstore_V1StGXR8Z5jdHi6B"
          },
          "memory_id": {
            "type": "string",
            "nullable": true,
            "description": "The memory this assertion resolved into: the new memory for `created` and `superseded`, the existing memory that matched for `skipped`, the memory withdrawn for `retracted`. Null only once that memory has been deleted.",
            "example": "mem_V1StGXR8Z5jdHi6B"
          },
          "superseded_memory_id": {
            "type": "string",
            "nullable": true,
            "description": "The memory this assertion retired, on a `superseded` outcome; null otherwise, a retraction included — it retires its own memory, which `memory_id` already names. Read back from `memories.superseded_by_memory_id` rather than stored here, because validity lives on the memory — where every read filters it — and exactly one memory is superseded per assertion.",
            "example": "mem_V1StGXR8Z5jdHi6B"
          },
          "content": {
            "type": "string",
            "description": "The text as asserted, which is not necessarily the memory's text: a `skipped` assertion records what was claimed, while the memory keeps what it already held.",
            "example": "The customer prefers email communication over phone calls"
          },
          "mechanism": {
            "type": "string",
            "enum": [
              "tool",
              "rule",
              "api",
              "formation"
            ],
            "description": "Which door the write came through. `tool` is the agent's `write_memory` call mid-turn; `rule` is a post-turn pass over the finished turn; `api` is `POST /api/v1/memories`; `formation` is a `memory` resource in an applied template. It answers *how*, never *who* — the principal fields answer that.",
            "example": "tool"
          },
          "rule_id": {
            "x-soat-ref": "memory-rules",
            "type": "string",
            "nullable": true,
            "description": "The memory rule whose firing wrote this, set only when `mechanism` is `rule` — the built-in extractor included, since that is a rule with no handler. Null once the rule has been deleted, and on assertions written before memory rules shipped.",
            "example": "mrule_V1StGXR8Z5jdHi6B"
          },
          "generation_id": {
            "x-soat-ref": "generations",
            "type": "string",
            "nullable": true,
            "description": "The turn that asserted the fact — the origin of anything an agent wrote, and the edge a conversation is reachable from. Null on the `api` and `formation` doors, which have no generation behind them.",
            "example": "gen_V1StGXR8Z5jdHi6B"
          },
          "principal_type": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who claimed the fact: `user` or `api_key` on the `api` and `formation` doors, `agent` on both agent doors — the extractor runs under the agent's identity."
          },
          "principal_id": {
            "type": "string",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "outcome": {
            "type": "string",
            "enum": [
              "created",
              "superseded",
              "skipped",
              "retracted"
            ],
            "description": "What the write resolved to. `created` is a distinct fact, `superseded` is the same fact changed (the top match was invalidated and replaced), `skipped` is a fact already known, `retracted` is a fact that stopped holding with nothing replacing it.",
            "example": "created"
          },
          "similarity": {
            "type": "number",
            "nullable": true,
            "description": "The cosine similarity the outcome was decided against — the top match's, or the declared target's when `declared` is true, where it decides nothing and only records how far apart the two statements were. Null when there was nothing to compare against, when the content could not be embedded, and on a `retracted` outcome, which compares nothing.",
            "example": 0.97
          },
          "declared": {
            "type": "boolean",
            "description": "Whether the write named the memory it replaced (`supersedes` on create) instead of the thresholds choosing one. Always false on a `created`, `skipped` or `retracted` outcome.",
            "example": false
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MemoryRuleEvent": {
        "type": "string",
        "enum": [
          "generations.completed",
          "conversation_messages.generated"
        ],
        "description": "The event a firing reads. `generations.completed` fires once per completed turn whatever the transport, and is the only event the built-in extractor may bind to. `conversation_messages.generated` fires per persisted assistant reply and is conversation-backed, so it is for custom handlers.",
        "example": "generations.completed"
      },
      "MemoryRule": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "mrule_V1StGXR8Z5jdHi6B"
          },
          "memory_store_id": {
            "x-soat-ref": "memory-stores",
            "type": "string"
          },
          "project_id": {
            "x-soat-ref": "projects",
            "type": "string"
          },
          "on": {
            "$ref": "#/components/schemas/MemoryRuleEvent"
          },
          "source_agent_ids": {
            "type": "array",
            "nullable": true,
            "items": {
              "x-soat-ref": "agents",
              "type": "string"
            }
          },
          "agent_id": {
            "x-soat-ref": "agents",
            "type": "string",
            "nullable": true
          },
          "tool_id": {
            "x-soat-ref": "tools",
            "type": "string",
            "nullable": true
          },
          "action": {
            "type": "string",
            "nullable": true
          },
          "preset_parameters": {
            "type": "object",
            "nullable": true
          },
          "prompt": {
            "type": "string",
            "nullable": true
          },
          "ai_provider_id": {
            "x-soat-ref": "ai-providers",
            "type": "string",
            "nullable": true
          },
          "model": {
            "type": "string",
            "nullable": true
          },
          "enabled": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MemoryStore": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "mstore_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": {
            "description": "Key-value tags for filtering in knowledge search.",
            "allOf": [
              {
                "$ref": "#/components/schemas/TagBag"
              }
            ]
          },
          "duplicate_threshold": {
            "type": "number",
            "nullable": true,
            "description": "The store's duplicate cutoff, or `null` when it has never been set. Reported as `null` rather than as the constant it resolves to: a store with no policy must read differently from one pinned to today's default.",
            "example": 0.95
          },
          "supersede_threshold": {
            "type": "number",
            "nullable": true,
            "description": "The store's supersede cutoff, or `null` when it has never been set.",
            "example": 0.9
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MetadataBag": {
        "type": "object",
        "description": "Caller-owned annotations on a resource, stored as the object they were written as: the types a value was written with are the types a read returns, so a filter can ask an ordering question about a number. Unlike other body fields, keys are stored and returned verbatim in the casing supplied — they are not converted between snake_case and camelCase.\n\nNo key is reserved, and that is the point: every piece of state the platform owns lives in its own typed column, so nothing written here reaches platform state. The platform never reads the bag — it is not an IAM context, not a policy input and not part of a prompt — which is what separates it from a tag bag.",
        "example": {
          "author": "John",
          "revision": 2
        }
      },
      "NullableMetadataBag": {
        "type": "object",
        "nullable": true,
        "description": "A `MetadataBag` on a request field where `null` is meaningful: a full-replacement update that clears the bag. A record whose bag is unset leaves the field out.",
        "example": {
          "author": "John",
          "revision": 2
        }
      },
      "MetadataBagText": {
        "type": "string",
        "description": "A `MetadataBag` as JSON text. A multipart field carries text, so the object travels serialized on those surfaces and is parsed on arrival; a JSON body carries the object itself. Text that is not a JSON object is `400 VALIDATION_FAILED`.",
        "example": "{\"author\":\"John\",\"revision\":2}"
      },
      "MetadataFilter": {
        "type": "object",
        "description": "Narrows results by the `metadata` bag. Each key is a field; its value is either a value to match exactly, or an object naming one or more operators.\n\nEquality and `in` match the stored value exactly, so `3` and `\"3\"` are different filters.\n\n`gt`, `gte`, `lt` and `lte` order the field against the operand, and the operand's own JSON type says which comparison is meant: a number orders numerically, a string lexicographically. They work on any field, in any scope. A document whose field holds another type is excluded from the comparison rather than erroring on it, so a range never fails on a bag that happens to hold text where another holds a number. An operand that is neither a number nor a string — a boolean, `null`, a list, an object — has no ordering and is `400 VALIDATION_FAILED`, naming the field in `meta.field`.",
        "additionalProperties": true,
        "example": {
          "quarter": "Q1",
          "revision": {
            "gte": 3,
            "lt": 11
          },
          "status": {
            "in": [
              "draft",
              "final"
            ]
          }
        }
      },
      "MetadataSchemaRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public ID (mdschema_ prefix)",
            "example": "mdschema_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-soat-ref": "projects",
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "resource_type": {
            "type": "string",
            "enum": [
              "document"
            ],
            "description": "The resource whose metadata this declaration governs.",
            "example": "document"
          },
          "path_prefix": {
            "type": "string",
            "description": "The selector, in the field its resource type is addressed by. A document's is the directory it is filed under.",
            "example": "/reports"
          },
          "schema": {
            "type": "object",
            "description": "The declared JSON Schema, as written.",
            "example": {
              "type": "object",
              "required": [
                "quarter"
              ],
              "properties": {
                "quarter": {
                  "type": "string"
                }
              }
            }
          },
          "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"
          }
        }
      },
      "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"
        ],
        "additionalProperties": false,
        "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\n`authorization_code` grant; `refresh_token` to the `refresh_token` grant.\n",
        "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\n`{ code, message, hint, docs_url }`, because an OAuth client parses `error` to decide what to\ndo next.\n",
        "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"
          }
        }
      },
      "OnBehalfOf": {
        "type": "object",
        "nullable": true,
        "description": "The tool call the write was made on behalf of, verified from the request's `On-Behalf-Of` header (a tool assertion this deployment signed, sent back with an API key scoped to its project); null for a write made as the credential alone. Attribution only: the `created_by_principal_*` pair and the permissions checked are the credential's.",
        "properties": {
          "tool_id": {
            "type": "string",
            "nullable": true,
            "example": "tool_V1StGXR8Z5jdHi6B"
          },
          "agent_id": {
            "type": "string",
            "nullable": true,
            "description": "The agent whose turn called the tool",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "generation_id": {
            "type": "string",
            "nullable": true,
            "description": "The generation that called the tool",
            "example": "gen_V1StGXR8Z5jdHi6B"
          },
          "session_id": {
            "type": "string",
            "nullable": true,
            "example": "sess_V1StGXR8Z5jdHi6B"
          },
          "actor_id": {
            "type": "string",
            "nullable": true,
            "example": "actor_V1StGXR8Z5jdHi6B"
          }
        }
      },
      "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 the per-project listing 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. Summed over the caller's own projects when the caller is project-scoped.",
            "example": 12
          },
          "claimed_tasks": {
            "type": "integer",
            "description": "Tasks currently claimed with a valid (unexpired) lease. Summed over the caller's own projects when the caller is project-scoped.",
            "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. Also `null` for a project-scoped caller: the figure is deployment-wide.",
            "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, and for a project-scoped caller: the window is deployment-wide.",
            "properties": {
              "p50": {
                "type": "number",
                "nullable": true,
                "example": 18
              },
              "p95": {
                "type": "number",
                "nullable": true,
                "example": 240
              },
              "window_seconds": {
                "type": "integer",
                "example": 300
              }
            }
          }
        }
      },
      "QueueProjectStats": {
        "type": "object",
        "description": "One project's share of the orchestration run queue.",
        "properties": {
          "project_id": {
            "type": "string",
            "description": "Public project ID (proj_ prefix).",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "queued": {
            "type": "integer",
            "description": "Tasks of this project waiting to be claimed now.",
            "example": 5
          },
          "claimed": {
            "type": "integer",
            "description": "Tasks of this project claimed with a valid lease.",
            "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, 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.\n"
          },
          "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."
          },
          "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.\n"
          },
          "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.\n"
          },
          "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.\n"
          },
          "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. An `agent` node's artifact always carries both `content` (the text response) and `object` (the parsed value when a schema applied, `null` otherwise), so `output.content` reads the same whether or not the node declares an `output_schema`.\n"
          },
          "output_schema": {
            "type": "object",
            "description": "For agent nodes — JSON Schema the model's answer is parsed into, reaching the artifact as `object`. Declaring it here is only needed for an agent that carries no `output_schema` of its own: the agent's schema already produces the artifact's `object` wherever that agent generates.\n"
          },
          "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."
          },
          "on_item_error": {
            "type": "string",
            "enum": [
              "fail",
              "collect"
            ],
            "description": "For loop nodes — what a failed item does. `fail` (the default) fails the node on the first item whose child run settles `failed`, `cancelled` or `expired`. `collect` keeps going: that item's entry in `results` becomes `{ \"error\": { \"code\", \"message\" }, \"orchestration_run_id\" }`, in its original position. Either way the artifact carries `failed_count`. A child run that could not be started (for example past the nesting depth bound) fails the node in both modes."
          },
          "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. 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).\n"
          },
          "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).\n"
          },
          "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).\n"
          },
          "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.\n"
          },
          "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.\n"
          },
          "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.\n"
          },
          "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).\n"
          },
          "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.\n",
            "additionalProperties": false,
            "properties": {
              "max_attempts": {
                "type": "integer",
                "description": "Total attempts including the first (default 1, ceiling 20).\n"
              },
              "backoff": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "strategy": {
                    "type": "string",
                    "enum": [
                      "fixed",
                      "exponential"
                    ],
                    "description": "`fixed` waits `delay_ms` between every attempt; `exponential` doubles per prior attempt. Default `fixed`.\n"
                  },
                  "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).\n"
                  }
                }
              }
            }
          }
        }
      },
      "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 come from a completed node before the target runs. Omitted, `all`."
          }
        }
      },
      "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.\n",
            "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)."
          },
          "output_mapping": {
            "$ref": "#/components/schemas/OrchestrationOutputMapping"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrchestrationOutputMapping": {
        "type": "object",
        "nullable": true,
        "additionalProperties": true,
        "description": "The shape of a completed run's `output`, owned by the orchestration rather than by its terminal node ids. Each key is an output field (a dotted key such as `summary.title` builds a nested object); each value is JSON Logic (https://jsonlogic.com) evaluated against `{ \"state\": <final run state> }`, which includes `state.input` and `state.nodes.<id>`. A missing path maps to `null`; a mapping that throws fails the run. Null (or omitted) keys `output` by terminal node id. Versioned with the graph; `null` on update clears it.",
        "example": {
          "summary": {
            "var": "state.summary"
          },
          "answer": {
            "var": "state.nodes.answer.content"
          }
        }
      },
      "CreateOrchestrationRequest": {
        "type": "object",
        "required": [
          "name",
          "nodes",
          "edges"
        ],
        "additionalProperties": false,
        "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": {
            "description": "The graph's nodes. Each `id` is unique and is what edges and `nodes.<id>` state references name; a node with no incoming edge starts the run.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrchestrationNode"
            }
          },
          "edges": {
            "description": "Directed edges between node `id`s. Every `from`/`to` must name a node in `nodes`, and the graph must be acyclic unless it contains a `loop` node. A node with several outgoing edges activates every target in parallel. `[]` for a graph of independent nodes.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrchestrationEdge"
            }
          },
          "state_schema": {
            "type": "object",
            "nullable": true
          },
          "input_schema": {
            "type": "object",
            "nullable": true
          },
          "output_mapping": {
            "$ref": "#/components/schemas/OrchestrationOutputMapping"
          },
          "version_label": {
            "type": "string",
            "description": "Optional tag for the version this create archives, e.g. `initial`.",
            "example": "initial"
          }
        }
      },
      "UpdateOrchestrationRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "nodes": {
            "description": "The graph's nodes. Each `id` is unique and is what edges and `nodes.<id>` state references name; a node with no incoming edge starts the run. Replaces the stored list; omitted, the stored nodes are kept and validated against the new `edges`.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrchestrationNode"
            }
          },
          "edges": {
            "description": "Directed edges between node `id`s. Every `from`/`to` must name a node in `nodes`, and the graph must be acyclic unless it contains a `loop` node. A node with several outgoing edges activates every target in parallel. Replaces the stored list; omitted, the stored edges are kept and validated against the new `nodes`.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrchestrationEdge"
            }
          },
          "state_schema": {
            "type": "object",
            "nullable": true
          },
          "input_schema": {
            "type": "object",
            "nullable": true
          },
          "output_mapping": {
            "$ref": "#/components/schemas/OrchestrationOutputMapping"
          },
          "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"
          },
          "expected_version": {
            "description": "Refuses the write unless the resource is at this version.",
            "allOf": [
              {
                "$ref": "#/components/schemas/ExpectedVersion"
              }
            ]
          }
        }
      },
      "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.\n\nDeliberately 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
              },
              "output_mapping": {
                "$ref": "#/components/schemas/OrchestrationOutputMapping"
              }
            }
          },
          "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_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who produced this version. Null for a write with no principal behind it."
          },
          "created_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RestoreOrchestrationVersionRequest": {
        "type": "object",
        "additionalProperties": false,
        "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}`.\n\nNull for runs created before pinning existed, which execute the live graph.\n",
            "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; `completed`/`failed`/ `cancelled` are terminal; `expired` is a wait that passed its deadline.",
            "enum": [
              "queued",
              "running",
              "sleeping",
              "awaiting_input",
              "completed",
              "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": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why it failed, 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."
          },
          "metadata": {
            "description": "The caller-owned key/value metadata supplied at run creation, returned verbatim. Absent 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"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          },
          "idempotency_key": {
            "type": "string",
            "nullable": true,
            "description": "The deduplication key the run was started under, unique within the project and claimed for as long as the run record exists. Null for a run started without one, which every platform-started run is.",
            "example": "dispatch-2026-09-18-activation-42"
          },
          "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."
          },
          "orchestration_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_orchestration_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": "What a completed run returns: the orchestration's `output_mapping` evaluated over the final state, or, when it declares none, the terminal node artifacts keyed by node id. Null unless the run completed."
          },
          "usage": {
            "allOf": [
              {
                "$ref": "#/components/schemas/UsageTotals"
              }
            ],
            "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 only on the single-run read (`GET /api/v1/orchestration-runs/{orchestration_run_id}`); omitted from run lists and from every write's response, including a start with `wait: true`.\n\nA 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/UsageTotals"
              }
            ],
            "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 only on the single-run read, like `usage`.\n\nThis 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
          },
          "pause_requested_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When an operator pause was requested, or null when none is in force. Independent of `status`: a `running` run keeps running until its next checkpoint, and a run parked on a node keeps that node's `required_action`."
          },
          "pause_reason": {
            "type": "string",
            "nullable": true,
            "description": "The reason supplied with the pause, when one was."
          },
          "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"
          }
        }
      },
      "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",
          "dispatches",
          "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).\n"
          },
          "dispatches": {
            "type": "integer",
            "description": "How many times this attempt was dispatched, including the first. A worker that stops mid-node leaves its task to be redelivered, and the redelivery re-runs the node under this same record — so a count above 1 is work the run issued, and was billed for, more than once.\n"
          },
          "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": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why it failed, 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": "Why an awaiting_input run is parked. `node_id`, `prompt` and `context` describe a node's own pause and are present for every kind but `paused`, which is an operator pause with no node of its own and carries `reason` instead.",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "human_input",
              "webhook_receive",
              "approval",
              "paused"
            ],
            "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",
            "description": "The node the run is parked at. Absent for a `paused` action, which no node produced."
          },
          "prompt": {
            "type": "string"
          },
          "context": {
            "type": "object"
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "Present for `paused` — the reason the operator gave, or null when they gave none."
          },
          "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."
          }
        }
      },
      "PauseOrchestrationRunRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "maxLength": 256,
            "description": "Why the run is being paused, surfaced on the parked run's `required_action.reason` and on `pause_reason`."
          }
        },
        "additionalProperties": false
      },
      "HumanInputRequest": {
        "type": "object",
        "required": [
          "node_id"
        ],
        "additionalProperties": false,
        "properties": {
          "node_id": {
            "type": "string",
            "description": "ID of the human node to satisfy."
          },
          "output": {
            "type": "object",
            "description": "Output/response provided by the human reviewer."
          }
        }
      },
      "StartOrchestrationRunRequest": {
        "type": "object",
        "required": [
          "orchestration_id"
        ],
        "additionalProperties": false,
        "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.\n\nThe 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. It is **write-only**: a run is a record every principal who may read runs can read, and a credential in it is not theirs to see, so it is never returned on one. It also does not outlive the run: reaching a terminal status (`completed`, `failed`, `cancelled` or `expired`) clears it. 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.\n\nThe 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": {
            "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.\n\nThe 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.\n\nIt 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"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          },
          "idempotency_key": {
            "type": "string",
            "maxLength": 255,
            "description": "Deduplication key, unique within the project, that makes a retry of an ambiguous failure safe. The first request under a key starts the run and answers `201`; any later request carrying the same key answers `200` with that same run, whatever state it has reached — including a retry that arrives while the original is still running.\n\nThe key is claimed by the run record and stays claimed for as long as the record exists, so it never silently expires and lets a second run through.\n\n`orchestration_id`, `input`, `tool_context` and `metadata` are the request the key names: reusing a key with any of them changed is `409 IDEMPOTENCY_KEY_REUSED` rather than a replay of a run that does something else. `wait` is not part of that comparison — it says how the caller waits, not what the run is — so a retry may flip it.",
            "example": "dispatch-2026-09-18-activation-42"
          },
          "wait": {
            "type": "boolean",
            "default": false,
            "description": "When true, block until the run reaches a terminal (completed/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",
        "additionalProperties": false,
        "properties": {
          "nodes": {
            "description": "The graph's nodes. Each `id` is unique and is what edges and `nodes.<id>` state references name; a node with no incoming edge starts the run. Checked exactly as create checks them, without persisting.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrchestrationNode"
            }
          },
          "edges": {
            "description": "Directed edges between node `id`s. Every `from`/`to` must name a node in `nodes`, and the graph must be acyclic unless it contains a `loop` node. A node with several outgoing edges activates every target in parallel. Checked exactly as create checks them, without persisting.",
            "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."
          }
        }
      },
      "OrchestrationValidationResult": {
        "type": "object",
        "required": [
          "valid",
          "problems",
          "warnings"
        ],
        "properties": {
          "valid": {
            "type": "boolean",
            "description": "True when there are no blocking problems."
          },
          "problems": {
            "type": "array",
            "description": "Every blocking problem — the items a refused write lists under `meta.problems`.",
            "items": {
              "$ref": "#/components/schemas/ValidationProblem"
            }
          },
          "warnings": {
            "type": "array",
            "description": "Problems that do not block a write, such as a state key written only on a conditional branch (`CONDITIONAL_STATE_KEY`). A refused write carries them under `meta.warnings`.",
            "items": {
              "$ref": "#/components/schemas/ValidationProblem"
            }
          }
        }
      },
      "PlaybookInputSource": {
        "description": "`manual` for an input a person or an integration writes, or the field of an earlier node it reads: `output[.path]`, or `input.<name>[.path]`. Sources are the playbook's edges.",
        "oneOf": [
          {
            "type": "string",
            "enum": [
              "manual"
            ]
          },
          {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "node_id",
              "field"
            ],
            "properties": {
              "node_id": {
                "type": "string",
                "description": "The ID of the node it reads",
                "example": "revenue"
              },
              "field": {
                "type": "string",
                "example": "output.annual_revenue"
              }
            }
          }
        ]
      },
      "PlaybookInput": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "schema",
          "required",
          "source"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Letters, digits and underscores, starting with a letter or underscore",
            "example": "revenue"
          },
          "schema": {
            "type": "object",
            "description": "The JSON Schema a value must satisfy",
            "example": {
              "type": "number"
            }
          },
          "required": {
            "type": "boolean",
            "description": "The node cannot run until a required input is set"
          },
          "description": {
            "type": "string",
            "description": "What to ask for; shown with the input when it is missing"
          },
          "source": {
            "$ref": "#/components/schemas/PlaybookInputSource"
          }
        }
      },
      "PlaybookExecutor": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "agent",
              "tool",
              "decider",
              "workflow",
              "orchestration"
            ]
          },
          "id": {
            "type": "string",
            "description": "The ID of the agent, tool, decider, workflow or orchestration",
            "example": "dcd_V1StGXR8Z5jdHi6B"
          },
          "input": {
            "type": "object",
            "description": "JSON Logic over the node's inputs, as an orchestration's `input_mapping`: each key is a field of the executor's input. Absent, the executor receives the node's input values as they are."
          }
        }
      },
      "PlaybookTransformation": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "format",
          "tool"
        ],
        "properties": {
          "name": {
            "type": "string",
            "example": "proposal"
          },
          "format": {
            "type": "string",
            "description": "The file's format; the tool must answer its content type: `application/pdf` for `pdf`, the Office Open XML types for `docx`, `xlsx` and `pptx`, `text/csv`, `text/html`, `text/markdown`, `text/plain`, `application/json`, `image/png` and `image/jpeg` for `jpg`",
            "enum": [
              "csv",
              "docx",
              "html",
              "jpg",
              "json",
              "md",
              "pdf",
              "png",
              "pptx",
              "txt",
              "xlsx"
            ],
            "example": "pdf"
          },
          "tool": {
            "type": "string",
            "description": "The tool that renders the final node's output as a file, answering with `execute.response_mode: base64`",
            "example": "tool_V1StGXR8Z5jdHi6B"
          }
        }
      },
      "PlaybookNode": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "title",
          "kind",
          "inputs"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable across versions: a case's values carry over by ID. Letters, digits and underscores, starting with a letter or underscore.",
            "example": "regime"
          },
          "title": {
            "type": "string",
            "example": "Tax regime"
          },
          "description": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "collect",
              "execute",
              "confirm"
            ],
            "description": "`collect` outputs its inputs, `execute` runs its executor, `confirm` waits for a person to confirm its inputs."
          },
          "final": {
            "type": "boolean",
            "description": "A goal of the playbook; only a final node carries transformations",
            "default": false
          },
          "inputs": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/PlaybookInput"
            }
          },
          "executor": {
            "$ref": "#/components/schemas/PlaybookExecutor"
          },
          "transformations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlaybookTransformation"
            }
          }
        }
      },
      "PlaybookDefinition": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "nodes"
        ],
        "properties": {
          "nodes": {
            "type": "array",
            "minItems": 1,
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/PlaybookNode"
            }
          }
        }
      },
      "Playbook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "pbk_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-soat-ref": "projects",
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "name": {
            "type": "string",
            "example": "client-pricing"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "version": {
            "type": "integer",
            "description": "The definition's version. Bumped only by a write that changes `definition`.",
            "example": 1
          },
          "definition": {
            "$ref": "#/components/schemas/PlaybookDefinition"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreatePlaybookRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "definition"
        ],
        "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": "client-pricing"
          },
          "description": {
            "description": "What the playbook is for",
            "type": "string",
            "nullable": true
          },
          "definition": {
            "description": "The nodes and how their inputs are sourced",
            "allOf": [
              {
                "$ref": "#/components/schemas/PlaybookDefinition"
              }
            ]
          },
          "version_label": {
            "type": "string",
            "description": "Optional tag for version 1, e.g. `initial`",
            "example": "initial"
          }
        }
      },
      "UpdatePlaybookRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "description": "Human-readable name, unique per project",
            "type": "string"
          },
          "description": {
            "description": "What the playbook is for",
            "type": "string",
            "nullable": true
          },
          "definition": {
            "description": "Replaces the whole definition; a change archives a new version",
            "allOf": [
              {
                "$ref": "#/components/schemas/PlaybookDefinition"
              }
            ]
          },
          "version_label": {
            "type": "string",
            "description": "Optional tag for the version this write archives. Ignored when the write changes no definition, since no version is archived."
          },
          "expected_version": {
            "description": "Refuses the write unless the playbook is at this version.",
            "allOf": [
              {
                "$ref": "#/components/schemas/ExpectedVersion"
              }
            ]
          }
        }
      },
      "ValidatePlaybookRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "definition"
        ],
        "properties": {
          "definition": {
            "description": "The definition to check",
            "allOf": [
              {
                "$ref": "#/components/schemas/PlaybookDefinition"
              }
            ]
          }
        }
      },
      "PlaybookValidationResult": {
        "type": "object",
        "required": [
          "valid",
          "problems"
        ],
        "properties": {
          "valid": {
            "type": "boolean"
          },
          "problems": {
            "type": "array",
            "description": "Every reason the definition cannot be stored: `DUPLICATE_NODE`, `DUPLICATE_INPUT`, `MISSING_NODE`, `UNKNOWN_FIELD`, `EXECUTOR_REQUIRED`, `EXECUTOR_FORBIDDEN`, `TRANSFORMATIONS_NOT_FINAL`, `DUPLICATE_TRANSFORMATION` or `CYCLE`. The same items a refused write lists under `meta.problems`.",
            "items": {
              "$ref": "#/components/schemas/ValidationProblem"
            }
          }
        }
      },
      "PlaybookVersion": {
        "type": "object",
        "description": "An immutable archive of a playbook's definition at one version.",
        "properties": {
          "id": {
            "type": "string",
            "example": "pbk_ver_V1StGXR8Z5jdHi6B"
          },
          "playbook_id": {
            "x-soat-ref": "playbooks",
            "type": "string",
            "example": "pbk_V1StGXR8Z5jdHi6B"
          },
          "version": {
            "type": "integer",
            "example": 1
          },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "description": "The versioned surface as it stood at this version.",
            "properties": {
              "definition": {
                "$ref": "#/components/schemas/PlaybookDefinition"
              }
            }
          },
          "label": {
            "type": "string",
            "nullable": true,
            "example": "restored from v2"
          },
          "created_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who produced this version. Null for a write with no principal behind it."
          },
          "created_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RestorePlaybookVersionRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "label": {
            "type": "string",
            "description": "Optional tag for the new version. Defaults to `restored from vN`.",
            "example": "rollback"
          }
        }
      },
      "CreatePlaybookCaseRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "subject"
        ],
        "properties": {
          "subject": {
            "type": "string",
            "description": "Who or what the case is about, as you name it",
            "example": "ACME Ltda (CNPJ 12.345.678/0001-90)"
          }
        }
      },
      "PlaybookNodeState": {
        "type": "string",
        "enum": [
          "blocked",
          "ready",
          "running",
          "waiting",
          "done",
          "stale",
          "failed",
          "rejected"
        ],
        "description": "`blocked` while a required input is unset or reads a node that is not `done`; `ready` when it can run; `running` while its execution is queued or runs its executor; `waiting` while it waits on the workflow task or orchestration run it started, named by `run`; `done` when its output is current; `stale` when an input changed after its output was produced; `failed` when its execution failed on its current inputs; `rejected` when a person rejected its current inputs."
      },
      "PlaybookNodeRun": {
        "type": "object",
        "required": [
          "type",
          "id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "task",
              "orchestration_run"
            ]
          },
          "id": {
            "type": "string",
            "description": "The task's or orchestration run's ID",
            "example": "task_V1StGXR8Z5jdHi6B"
          },
          "state": {
            "type": "string",
            "nullable": true,
            "description": "A task's workflow state; absent for a run, null once the task no longer exists"
          },
          "automation_status": {
            "type": "string",
            "nullable": true,
            "description": "A task's automation status: `failed` when its state's dispatch failed and no `on_failure` routes it, so the task waits on a person to move it. Absent for a run."
          },
          "status": {
            "type": "string",
            "nullable": true,
            "description": "A run's status, such as `awaiting_input`; absent for a task, null once the run no longer exists"
          }
        }
      },
      "PlaybookCaseNode": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "regime"
          },
          "state": {
            "$ref": "#/components/schemas/PlaybookNodeState"
          },
          "missing": {
            "type": "array",
            "description": "Required inputs that are unset or refused by their schema",
            "items": {
              "type": "string"
            }
          },
          "blocked_by": {
            "type": "array",
            "description": "Nodes it reads that must be `done` first",
            "items": {
              "type": "string"
            }
          },
          "run": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PlaybookNodeRun"
              }
            ],
            "description": "The task or run a `waiting` node waits on, read live on every case read; null in every other state."
          },
          "inputs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "value": {
                  "description": "The value it reads now; null when unset"
                },
                "revision": {
                  "type": "integer",
                  "nullable": true,
                  "description": "The revision of the entry the value comes from"
                }
              }
            }
          },
          "output": {
            "type": "object",
            "nullable": true,
            "properties": {
              "value": {
                "description": "The node's latest output"
              },
              "revision": {
                "type": "integer"
              }
            }
          }
        }
      },
      "PlaybookCaseSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "pbk_case_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-soat-ref": "projects",
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "playbook_id": {
            "x-soat-ref": "playbooks",
            "type": "string",
            "example": "pbk_V1StGXR8Z5jdHi6B"
          },
          "playbook_version": {
            "type": "integer",
            "description": "The version the case was created on and is evaluated against",
            "example": 1
          },
          "subject": {
            "type": "string"
          },
          "revision": {
            "type": "integer",
            "description": "The case's revision counter; every entry takes the next value",
            "example": 0
          },
          "automation_status": {
            "$ref": "#/components/schemas/PlaybookAutomationStatus"
          },
          "pause_requested_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the case was paused; null while it runs"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PlaybookAutomationStatus": {
        "type": "string",
        "description": "`paused` while a pause of the case or of its project stands, else `running` while an execution is queued or runs its executor, else `waiting` while every execution in flight waits on a workflow task or orchestration run it started, else `idle`",
        "enum": [
          "idle",
          "running",
          "waiting",
          "paused"
        ]
      },
      "PlaybookCase": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PlaybookCaseSummary"
          },
          {
            "type": "object",
            "properties": {
              "nodes": {
                "type": "array",
                "description": "Every node of the case's version, in definition order",
                "items": {
                  "$ref": "#/components/schemas/PlaybookCaseNode"
                }
              }
            }
          }
        ]
      },
      "PlaybookInputWrite": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "node_id",
          "name",
          "value",
          "expected_revision"
        ],
        "properties": {
          "node_id": {
            "type": "string",
            "example": "revenue"
          },
          "name": {
            "type": "string",
            "example": "revenue"
          },
          "value": {
            "description": "The value, which must satisfy the input's schema",
            "example": 4000000
          },
          "expected_revision": {
            "type": "integer",
            "nullable": true,
            "description": "The revision of the field the writer read, or `null` for a field it read as unset. Any other current revision refuses the batch.",
            "example": null
          }
        }
      },
      "WritePlaybookCaseInputsRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "inputs"
        ],
        "properties": {
          "inputs": {
            "description": "The writes, applied all or none",
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/PlaybookInputWrite"
            }
          },
          "wait": {
            "type": "boolean",
            "default": false,
            "description": "Answer once every execution the write queued, and every one those queue in turn, has settled or parked, instead of right away"
          }
        }
      },
      "PlaybookMissingInput": {
        "type": "object",
        "properties": {
          "node_id": {
            "type": "string"
          },
          "input": {
            "type": "string"
          },
          "schema": {
            "type": "object",
            "description": "The JSON Schema a value must satisfy"
          },
          "description": {
            "type": "string"
          },
          "required": {
            "type": "boolean",
            "description": "Whether the node waits for this input; an optional one never blocks it"
          }
        }
      },
      "PlaybookCaseMissing": {
        "type": "object",
        "properties": {
          "case_id": {
            "type": "string",
            "example": "pbk_case_V1StGXR8Z5jdHi6B"
          },
          "node_id": {
            "type": "string",
            "example": "pricing"
          },
          "waves": {
            "type": "array",
            "description": "Missing inputs grouped by the wave of the node that holds them",
            "items": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/PlaybookMissingInput"
              }
            }
          }
        }
      },
      "PlaybookEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "pbk_ent_V1StGXR8Z5jdHi6B"
          },
          "revision": {
            "type": "integer"
          },
          "node_id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "input",
              "output",
              "confirmation",
              "rejection"
            ]
          },
          "input": {
            "type": "string",
            "nullable": true,
            "description": "The input written, on an `input` entry"
          },
          "value": {
            "description": "The value written"
          },
          "basis": {
            "type": "object",
            "nullable": true,
            "description": "What an output, confirmation or rejection was decided on"
          },
          "playbook_version": {
            "type": "integer"
          },
          "created_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who recorded this entry; null for one the case's own cascade recorded."
          },
          "created_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "on_behalf_of": {
            "$ref": "#/components/schemas/OnBehalfOf"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RunPlaybookCaseNodeRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "wait": {
            "type": "boolean",
            "default": false,
            "description": "Answer once the executor settles or parks instead of right away"
          }
        }
      },
      "PlaybookExecution": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "pbk_exe_V1StGXR8Z5jdHi6B"
          },
          "case_id": {
            "type": "string",
            "example": "pbk_case_V1StGXR8Z5jdHi6B"
          },
          "node_id": {
            "type": "string",
            "description": "The ID of the node that ran",
            "example": "revenue"
          },
          "playbook_version": {
            "type": "integer",
            "description": "The playbook version the case runs on"
          },
          "executor": {
            "type": "object",
            "nullable": true,
            "description": "What ran; null for a `collect` node",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "agent",
                  "tool",
                  "decider",
                  "workflow",
                  "orchestration"
                ]
              },
              "id": {
                "type": "string",
                "description": "The ID of the agent, tool, decider, workflow or orchestration",
                "example": "agent_V1StGXR8Z5jdHi6B"
              }
            }
          },
          "status": {
            "type": "string",
            "description": "`queued` until a worker claims it, `running` while its executor runs or a task or run it started is open, then `completed` or `failed`",
            "enum": [
              "queued",
              "running",
              "completed",
              "failed"
            ]
          },
          "result": {
            "type": "string",
            "nullable": true,
            "enum": [
              "current",
              "superseded",
              null
            ],
            "description": "Once settled: `superseded` when the node's inputs changed while it ran, so its output was not written"
          },
          "run_id": {
            "type": "string",
            "nullable": true,
            "description": "The generation, decision, task or orchestration run it started"
          },
          "input": {
            "description": "What the executor was given"
          },
          "output": {
            "description": "What the executor answered; null until it succeeds"
          },
          "output_revision": {
            "type": "integer",
            "nullable": true,
            "description": "The revision of the output entry it wrote; null when it wrote none"
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why it failed: `{ code, message, meta? }`"
          },
          "created_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who asked for the run; null when the case started it."
          },
          "created_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "on_behalf_of": {
            "$ref": "#/components/schemas/OnBehalfOf"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "ExportPlaybookCaseNodeRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "transformations": {
            "type": "array",
            "minItems": 1,
            "description": "The names of the transformations to render; every one the node declares when omitted",
            "items": {
              "type": "string",
              "example": "proposal"
            }
          },
          "revision": {
            "type": "integer",
            "description": "The output or confirmation of the node to render; its current output when omitted"
          },
          "wait": {
            "type": "boolean",
            "default": false,
            "x-soat-tool-forced": true,
            "description": "Answer once every export settles instead of right away. A tool call, `builtin` or MCP, always waits."
          }
        }
      },
      "PlaybookExport": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "pbk_exp_V1StGXR8Z5jdHi6B"
          },
          "case_id": {
            "type": "string",
            "example": "pbk_case_V1StGXR8Z5jdHi6B"
          },
          "node_id": {
            "type": "string",
            "description": "The ID of the final node rendered",
            "example": "pricing"
          },
          "revision": {
            "type": "integer",
            "description": "The output or confirmation entry rendered"
          },
          "playbook_version": {
            "type": "integer",
            "description": "The playbook version whose transformation rendered it"
          },
          "transformation": {
            "type": "string",
            "example": "proposal"
          },
          "format": {
            "type": "string",
            "example": "pdf"
          },
          "tool_id": {
            "type": "string",
            "example": "tool_V1StGXR8Z5jdHi6B"
          },
          "status": {
            "type": "string",
            "description": "`running` while its tool runs, then `completed` or `failed`",
            "enum": [
              "running",
              "completed",
              "failed"
            ]
          },
          "stale": {
            "type": "boolean",
            "description": "Whether `revision` is no longer the node's current output, or the node is no longer `done`"
          },
          "file_id": {
            "type": "string",
            "nullable": true,
            "x-soat-ref": "files",
            "description": "The file rendered; null until it succeeds, and once the file is deleted",
            "example": "file_V1StGXR8Z5jdHi6B"
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why it failed: `{ code, message, meta? }`"
          },
          "created_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who asked for the export."
          },
          "created_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "on_behalf_of": {
            "$ref": "#/components/schemas/OnBehalfOf"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "UpgradePlaybookCaseRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "version": {
            "type": "integer",
            "description": "The version to move the case to; the playbook's current one when omitted",
            "example": 2
          },
          "wait": {
            "type": "boolean",
            "default": false,
            "description": "Answer once every execution the write queued, and every one those queue in turn, has settled or parked, instead of right away"
          }
        }
      },
      "PlaybookUpgrade": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "pbk_upg_V1StGXR8Z5jdHi6B"
          },
          "case_id": {
            "type": "string",
            "example": "pbk_case_V1StGXR8Z5jdHi6B"
          },
          "from_version": {
            "type": "integer",
            "description": "The version the case was on"
          },
          "to_version": {
            "type": "integer",
            "description": "The version the case moved to"
          },
          "diff": {
            "type": "object",
            "description": "What moved between the two versions, by node ID",
            "properties": {
              "added": {
                "type": "array",
                "description": "Nodes the new version adds",
                "items": {
                  "type": "string"
                }
              },
              "removed": {
                "type": "array",
                "description": "Nodes the new version drops",
                "items": {
                  "type": "string"
                }
              },
              "changed": {
                "type": "array",
                "description": "Nodes in both whose executor or inputs changed; a title, description or transformation change is not listed",
                "items": {
                  "type": "object",
                  "properties": {
                    "node_id": {
                      "type": "string"
                    },
                    "executor": {
                      "type": "boolean",
                      "description": "Whether the node's executor changed"
                    },
                    "inputs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "change": {
                            "type": "string",
                            "enum": [
                              "added",
                              "removed",
                              "source",
                              "schema",
                              "required"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "created_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who upgraded the case."
          },
          "created_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "on_behalf_of": {
            "$ref": "#/components/schemas/OnBehalfOf"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ConfirmPlaybookCaseNodeRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "decision",
          "expected_revisions"
        ],
        "properties": {
          "decision": {
            "type": "string",
            "description": "Whether the node's inputs, as read, are approved",
            "enum": [
              "approve",
              "reject"
            ]
          },
          "reason": {
            "type": "string",
            "description": "Why the inputs were rejected; required on a rejection",
            "example": "The revenue does not match the filed statement"
          },
          "expected_revisions": {
            "type": "object",
            "description": "Each of the node's inputs, by name, mapped to the revision read, or null for one read as unset",
            "additionalProperties": {
              "type": "integer",
              "nullable": true
            },
            "example": {
              "revenue": 3
            }
          },
          "wait": {
            "type": "boolean",
            "default": false,
            "description": "Answer once every execution the write queued, and every one those queue in turn, has settled or parked, instead of right away"
          }
        }
      },
      "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. A context key must be one the platform supplies — `soat:ResourceType` or `soat:ResourceTag/<tag>` — because an unknown key is absent from the evaluation context, so a `Deny` naming one would never deny. The tag name after the slash is opaque and unconstrained. 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`.\n",
            "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"
          }
        }
      },
      "PrincipalType": {
        "type": "string",
        "enum": [
          "user",
          "api_key",
          "agent",
          "automation",
          "approval",
          "system"
        ],
        "description": "What kind of principal acted. `user` and `api_key` are authenticated callers, and the id is the user's (`user_…`) or the key's own (`key_…`), so a record names which key acted rather than its owner. `agent` is an agent writing under its own identity (`agent_…`). `automation` is the task engine acting on a dispatch outcome and `approval` an approval resolution; neither carries an id. `system` is the platform itself, with an id naming the mechanism (`retention_sweep`, `zero_retention`).",
        "example": "user"
      },
      "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_orchestration_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
          },
          "require_priced_model": {
            "type": "boolean",
            "description": "Whether a generation whose model carries no price row is refused with `409 MODEL_NOT_PRICED`. `false` by default, where an unpriced model runs and meters at `cost_usd: null`.",
            "example": false
          },
          "default_conversation_retrieval": {
            "type": "string",
            "enum": [
              "embed",
              "none"
            ],
            "description": "What a conversation that names no `retrieval` of its own does. `none` by default.",
            "example": "none"
          },
          "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"
          },
          "paused_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the project was paused. `null` while it runs. While set, nothing new starts in the project — see pause-project.",
            "example": null
          },
          "pause_reason": {
            "type": "string",
            "nullable": true,
            "maxLength": 256,
            "description": "The reason the pause was requested with, echoed as `pause_reason` on every run and task the pause parked. `null` while the project runs, or when the pause named none.",
            "example": null
          },
          "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"
          }
        }
      },
      "PauseProjectRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "reason": {
            "type": "string",
            "maxLength": 256,
            "description": "Why the project is paused. Stored as `pause_reason` and carried onto every run and task the pause parks.",
            "example": "anomaly detected by the spend monitor"
          }
        }
      },
      "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": "`llm_tokens` for a token row or an agent's resource row, `tool_execution` for a tool's"
          },
          "resource": {
            "type": "string",
            "nullable": true,
            "description": "The tool or agent SRN a resource row prices; null for a SKU row"
          },
          "quantity": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "A resource row's quantity expression; null prices one per call"
          },
          "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`, `cache_write_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"
        ],
        "additionalProperties": false,
        "properties": {
          "prices": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "component",
                "unit",
                "unit_price",
                "effective_from"
              ],
              "additionalProperties": false,
              "properties": {
                "meter_type": {
                  "type": "string",
                  "enum": [
                    "llm_tokens",
                    "tool_execution"
                  ],
                  "default": "llm_tokens",
                  "description": "`llm_tokens` for a token row naming `provider` and `model`, or for an agent's resource row; `tool_execution` for a tool's resource row."
                },
                "resource": {
                  "type": "string",
                  "description": "The SRN of one tool or agent, `srn:<project_id>:<tool|agent>:<id>`, for a resource row; omit `provider` and `model`. A tool's row prices its `tool_execution` events, beside `tool_call`; an agent's prices its `llm_tokens` events, beside the provider cost. A client tool has no execution of its own and cannot be priced.",
                  "example": "srn:proj_V1StGXR8Z5jdHi6B:tool:tool_V1StGXR8Z5jdHi6B"
                },
                "quantity": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "On a resource row only: JSON Logic giving the quantity `unit_price` multiplies, over `{ input, action, response, outcome, duration_ms }` for a tool and `{ response: { usage, cost_usd, steps, tool_calls, stop_reason }, outcome }` for an agent. Omit to price one per call. A result that is not a finite number >= 0 records quantity `0` and no cost.",
                  "example": {
                    "var": "response.page_count"
                  }
                },
                "provider": {
                  "type": "string",
                  "description": "The provider slug of a token row",
                  "example": "openai"
                },
                "model": {
                  "type": "string",
                  "description": "The model of a token row",
                  "example": "gpt-4o"
                },
                "component": {
                  "type": "string",
                  "description": "The component this row prices: a token component (`input_tokens`, `output_tokens`, `cached_tokens`, `cache_write_tokens`), or a resource row's own (`page`)"
                },
                "unit": {
                  "type": "string",
                  "description": "`token` for token pricing; the resource component's unit otherwise"
                },
                "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"
                }
              }
            }
          }
        }
      },
      "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",
              "storage_bytes"
            ]
          },
          "window": {
            "type": "string",
            "enum": [
              "rolling_1m",
              "rolling_1h",
              "rolling_24h",
              "calendar_month",
              "current"
            ],
            "description": "The window the metric is aggregated over; `current` on storage_bytes, which caps a stored total and never resets."
          },
          "limit": {
            "type": "number"
          },
          "mode": {
            "type": "string",
            "enum": [
              "enforce",
              "monitor"
            ]
          },
          "meter_type": {
            "type": "string",
            "enum": [
              "llm_tokens",
              "compute_execution",
              "api_request",
              "storage",
              "tool_execution",
              null
            ],
            "nullable": true,
            "description": "The meter a cost_usd cap answers for. Null is every priced meter, which is what a quota created without one carries."
          },
          "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, and for a partly priced window, which no posture refuses). 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), null for storage_bytes (a stored total has no window and no counter — read the footprint from the storage meter), and null 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, kept after a shared agent is deleted by its owner.\n",
            "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\n",
            "example": "actor_V1StGXR8Z5jdHi6B"
          },
          "tags": {
            "$ref": "#/components/schemas/TagBag"
          },
          "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"
          },
          "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).\n",
            "example": 3
          },
          "max_context_messages": {
            "type": "integer",
            "nullable": true,
            "default": null,
            "description": "Maximum number of recent conversation messages sent to the model on each generation. Takes precedence over the agent's `max_context_messages`; null defers to it. A fork keeps its parent's value.\n",
            "example": 20
          },
          "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.\n",
            "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.\n",
            "example": 7
          },
          "usage": {
            "allOf": [
              {
                "$ref": "#/components/schemas/UsageTotals"
              }
            ],
            "description": "What the session's generations cost: token counts and `cost_usd` summed across every metered generation dispatched through it. Present on the single-session read; omitted from session and fork list responses.\n\nA fork is a session of its own, so it starts at zero rather than inheriting what the history it copied cost — summing `usage` across a session and its forks therefore never double-counts. Work the platform does around a session without a generation of its own (a memory extraction pass, for instance) is metered on the project, not here."
          }
        }
      },
      "ForkSessionRequest": {
        "type": "object",
        "additionalProperties": false,
        "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).\n",
            "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.\n",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "name": {
            "type": "string",
            "description": "Optional name for the forked session",
            "example": "retry with stricter system prompt"
          },
          "tags": {
            "$ref": "#/components/schemas/TagBag"
          },
          "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.\n"
          }
        }
      },
      "CreateSessionRequest": {
        "type": "object",
        "required": [
          "agent_id"
        ],
        "additionalProperties": false,
        "properties": {
          "agent_id": {
            "x-soat-ref": "agents",
            "type": "string",
            "description": "Agent this session belongs to. With a credential scoped to a project, it may also be an agent another project shares with that project; the session is then that project's.\n",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "name": {
            "type": "string",
            "description": "Optional session name",
            "example": "Support chat"
          },
          "tags": {
            "$ref": "#/components/schemas/TagBag"
          },
          "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.\n",
            "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 stored exactly as sent and never case-converted. 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`. `session_id`, `actor_id` and `actor_external_id` are server-derived and dropped from the stored bag, in any casing. Write-only: no read of a session returns it. It also does not outlive the session: closing or expiring it clears the bag."
          },
          "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).\n",
            "example": 3
          },
          "max_context_messages": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "default": null,
            "description": "Maximum number of recent conversation messages sent to the model on each generation, taking precedence over the agent's `max_context_messages`. null or absent defers to the agent.\n",
            "example": 20
          }
        }
      },
      "UpdateSessionRequest": {
        "type": "object",
        "additionalProperties": false,
        "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 stored exactly as sent and never case-converted. 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`. `session_id`, `actor_id` and `actor_external_id` are server-derived and dropped from the stored bag, in any casing. Write-only: no read of a session returns it. It also does not outlive the session: closing or expiring it clears the bag."
          },
          "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.\n",
            "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.\n",
            "example": 3
          },
          "max_context_messages": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Maximum number of recent conversation messages sent to the model on each generation, taking precedence over the agent's `max_context_messages`. Set to null to defer to the agent.\n",
            "example": 20
          }
        }
      },
      "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. Reusing the key with a different `message`, `document_id` or `tool_context` is `409 IDEMPOTENCY_KEY_REUSED`.\n",
                "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. Reusing the key with a different `message`, `document_id` or `tool_context` is `409 IDEMPOTENCY_KEY_REUSED`.\n",
                "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/GenerateConversationMessageResponse"
          }
        ]
      },
      "GenerateSessionRequest": {
        "type": "object",
        "additionalProperties": false,
        "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`."
          }
        }
      },
      "SubmitSessionToolOutputsRequest": {
        "type": "object",
        "required": [
          "generation_id",
          "tool_outputs"
        ],
        "additionalProperties": false,
        "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"
              ],
              "additionalProperties": false,
              "properties": {
                "tool_call_id": {
                  "type": "string"
                },
                "output": {
                  "description": "The tool output value"
                }
              }
            }
          }
        }
      },
      "ShareCap": {
        "type": "object",
        "description": "The calls each acceptance of a share may make per fixed window. Windows are fixed and aligned like a quota's: `rolling_1h` resets on the hour.",
        "required": [
          "calls",
          "window"
        ],
        "additionalProperties": false,
        "properties": {
          "calls": {
            "type": "integer",
            "minimum": 1,
            "description": "Calls admitted per window",
            "example": 1000
          },
          "window": {
            "type": "string",
            "enum": [
              "rolling_1m",
              "rolling_1h",
              "rolling_24h",
              "calendar_month"
            ],
            "description": "The fixed window the calls are counted in"
          }
        }
      },
      "ShareReference": {
        "type": "object",
        "description": "One of the grantee project's resources naming the shared resource.",
        "required": [
          "type",
          "id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "agent",
              "tool",
              "ingestion_rule",
              "orchestration",
              "trigger",
              "formation"
            ],
            "description": "The kind of the grantee's resource."
          },
          "id": {
            "type": "string",
            "description": "Its public ID.",
            "example": "agent_V1StGXR8Z5jdHi6B"
          }
        }
      },
      "Share": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "shr_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-soat-ref": "projects",
            "type": "string",
            "description": "The publishing project"
          },
          "resource": {
            "type": "string",
            "description": "SRN of the shared resource",
            "example": "srn:proj_V1StGXR8Z5jdHi6B:tool:tool_V1StGXR8Z5jdHi6B"
          },
          "actions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The actions the share grants"
          },
          "grantee": {
            "type": "string",
            "description": "The project the share is offered to, or `*`"
          },
          "cap": {
            "description": "The calls each accepting project may make per window, or `null` for none",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/ShareCap"
              }
            ]
          },
          "suspended_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Set while the share is suspended"
          },
          "revoked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Set once the publisher revoked the share"
          },
          "acceptance": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ShareAcceptance"
              }
            ],
            "nullable": true,
            "description": "On a grantee's read only: that project's acceptance, or `null` when it has none"
          },
          "projection": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "On a single-share read only: what a grantee sees of the resource — `id`, `name`, `description` and `parameters` for a tool; `id` and `name` for an agent. `null` once the resource is gone."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ShareAcceptance": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "shr_acc_V1StGXR8Z5jdHi6B"
          },
          "share_id": {
            "type": "string",
            "example": "shr_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-soat-ref": "projects",
            "type": "string",
            "description": "The accepting project"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "revoked"
            ]
          },
          "revoked_by": {
            "type": "string",
            "nullable": true,
            "enum": [
              "publisher",
              "consumer",
              null
            ],
            "description": "Who revoked the acceptance. `publisher` refuses a new accept from the same project until the publisher deletes the acceptance."
          },
          "accepted_at": {
            "type": "string",
            "format": "date-time"
          },
          "revoked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TagBag": {
        "type": "object",
        "additionalProperties": {
          "type": "string"
        },
        "x-cli-flag-name": "tags",
        "description": "Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter, knowledge search, and the IAM `soat:ResourceTag/<key>` condition.\n\nKeys beginning `system.` are reserved: the platform writes them to record which conversation, actor, agent and role a row came from, and a write naming one is refused with `400 RESERVED_TAG_KEY`. They are read and filtered like any other tag.\n\nThe bag is bounded, because every pair reaches the IAM context of every access check on the resource: at most 50 keys, each key at most 128 characters and each value at most 256. A write past a bound — including a merge that would grow the stored bag past the key count — is `400 VALIDATION_FAILED` with `meta.limit` naming the bound it crossed. `system.*` keys are the platform's and do not count against the 50.",
        "example": {
          "team": "finance",
          "env": "prod"
        }
      },
      "NullableTagBag": {
        "type": "object",
        "nullable": true,
        "additionalProperties": {
          "type": "string"
        },
        "description": "A `TagBag` on a request field where `null` is meaningful: a full-replacement update that clears the bag. A record whose bag is unset leaves the field out.",
        "example": {
          "team": "finance",
          "env": "prod"
        }
      },
      "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": {
            "description": "The caller-owned key/value metadata supplied when the task was created, returned verbatim. Absent 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"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          },
          "last_result": {
            "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. An `agent` dispatch's result is the generation's output plus `ai_provider_id`, the provider that served its `model` (null when the route that ran named no serving target)."
          },
          "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",
              "paused",
              null
            ],
            "description": "Status of the current state's dispatch. `null` until a state with an automation is entered. `paused` means an operator pause suppressed this state's `on_enter` before it ran, so resume-task will start it."
          },
          "pause_requested_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When an operator paused this task's automation, or null when no pause is in force."
          },
          "pause_reason": {
            "type": "string",
            "nullable": true,
            "description": "The reason supplied with the pause, when one was."
          },
          "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_type": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who made the move: `user` or `api_key` for an authenticated caller, `automation` for the engine acting on an `on_enter` dispatch outcome, `approval` for an approval resolution. Named `principal_*`, not `actor_*`: these ids never reference the Actors module.\n"
          },
          "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.\n"
          },
          "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.\n"
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "from_workflow_version": {
            "type": "integer",
            "nullable": true,
            "description": "Set on a migration row only: the version the task left, or null for a task that had no pinned version."
          },
          "to_workflow_version": {
            "type": "integer",
            "nullable": true,
            "description": "Set on a migration row only, the version the task moved onto."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TaskTransitionOption": {
        "type": "object",
        "required": [
          "name",
          "to",
          "requires_approval",
          "allowed",
          "error_code"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "to": {
            "type": "string",
            "description": "The state the transition moves the task to."
          },
          "requires_approval": {
            "type": "boolean",
            "description": "Firing it parks an approval instead of moving the task."
          },
          "allowed": {
            "type": "boolean",
            "description": "Whether firing it now as the caller would be accepted."
          },
          "error_code": {
            "type": "string",
            "nullable": true,
            "enum": [
              "TASK_TRANSITION_CONFLICT",
              "TASK_GUARD_REJECTED",
              null
            ],
            "description": "The code firing it would answer: `TASK_TRANSITION_CONFLICT` when the task is closed, has a transition pending approval, or is not in one of the transition's `from` states; `TASK_GUARD_REJECTED` when its guard fails. Null when `allowed`."
          }
        }
      },
      "CreateTaskRequest": {
        "type": "object",
        "required": [
          "workflow_id",
          "title"
        ],
        "additionalProperties": false,
        "properties": {
          "project_id": {
            "type": "string"
          },
          "workflow_id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "payload": {
            "type": "object"
          },
          "assignee": {
            "type": "string",
            "nullable": true
          },
          "idempotency_key": {
            "type": "string",
            "maxLength": 255,
            "description": "The create's deduplication key, unique within the project and claimed for as long as the task it created exists. The same key with the same body returns that task as it is now (`200`) instead of creating a second one; with a different body it is `409 IDEMPOTENCY_KEY_REUSED`. A create that fails claims no key. Equivalent to the `Idempotency-Key` header."
          },
          "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.\nCreation 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`).\nThe 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).\nWrite-only: the stored bag is never returned by any task read, and it is cleared when the task reaches a terminal state."
          },
          "metadata": {
            "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).\n\nThe 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.\n\nPrefer 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"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/MetadataBag"
              }
            ]
          }
        }
      },
      "UpdateTaskRequest": {
        "type": "object",
        "additionalProperties": false,
        "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
          }
        }
      },
      "PauseTaskRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "maxLength": 256,
            "description": "Why the task is being paused, surfaced on `pause_reason`."
          }
        },
        "additionalProperties": false
      },
      "MigrateTaskRequest": {
        "type": "object",
        "required": [
          "to_version"
        ],
        "properties": {
          "to_version": {
            "type": "integer",
            "minimum": 1,
            "description": "The workflow version to move the task onto; newer than its current one."
          },
          "state_map": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Current state name to a state the target version declares. Required only for a state the target lacks; an entry for a state both declare overrides the identity mapping."
          }
        },
        "additionalProperties": false
      },
      "TransitionTaskRequest": {
        "type": "object",
        "required": [
          "transition"
        ],
        "additionalProperties": false,
        "properties": {
          "transition": {
            "type": "string"
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "payload": {
            "type": "object",
            "description": "Keys shallow-merged over the task's payload under the transition's lock, before the guard reads it, exactly as `PATCH` merges them; the result must satisfy the payload schema of the version the task is pinned to (`TASK_PAYLOAD_INVALID`). A refused move writes none of it. On a `requires_approval` transition the payload is held with the gate and merged when the approval fires the move, so a rejected or expired gate writes nothing."
          },
          "idempotency_key": {
            "type": "string",
            "maxLength": 255,
            "description": "The move's deduplication key, unique within the task and claimed for as long as the task exists. The same key with the same body replays the task as it is now instead of moving it again, whether the move applied or parked an approval; with a different body it is `409 IDEMPOTENCY_KEY_REUSED`. Equivalent to the `Idempotency-Key` header."
          },
          "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.\nSupplying 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.\nThe 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).\nWrite-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`, `body_mode` and `response_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). `response_mode` is `json` (default: the body parsed as JSON, else returned as text) or `base64`, which returns a 2xx body as `{ content_type, filename, data_base64 }` — the same file shape `multipart` accepts, so a pipeline step can relay a downloaded file. `filename` comes from `Content-Disposition` and is omitted when the target names none; a body over `TOOL_RESPONSE_MAX_BYTES` is `502 TOOL_RESPONSE_TOO_LARGE`.\n\n`auth` adds a computed request credential, for targets whose `Authorization` value cannot be expressed as a static header. Supported `auth.type` values:\n\n- `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.\n- `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.\n\nCredential 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.\n\nA credential slot takes a reference, never a literal: a literal `secret_access_key`, `session_token` or `credentials` under `auth`, a literal credential-named `headers` value (`Authorization`, `Cookie`, or a name containing `api-key`/`token`/`secret`/`password`), or a `url` carrying a password or a literal credential-named query parameter (those names, plus `key` and `sig`/`signature`) is refused with `400 LITERAL_CREDENTIAL`, `meta.paths` naming each one. A stored literal reads back as `{\"no_echo\": true}` (a `url` holding one is masked whole), an object rather than a string so a read-edit-write round trip fails the schema check instead of writing the placeholder in as the credential. A value carrying a `{{secret:...}}` reference is the wiring, not the credential, and stays readable.\n\n`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.\n"
          },
          "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. A literal credential in `headers` or `url` is refused, exactly as in `execute`."
          },
          "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. 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. On an `mcp` tool a preset reaches each action whose input schema declares it; an action whose schema declares no properties receives every one.\n\nValues 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; `steps` may be empty when `output` is set, which computes `output` over the input alone. A literal credential in an inline step `tool` is refused, exactly as in `execute`."
          },
          "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>, action: <action> }`, so `{ \"var\": \"output.text\" }` extracts a bare scalar field instead of requiring a wrapping `pipeline` tool, `{ \"var\": \"input.title\" }` echoes back a field of the request that produced the response, and `{ \"var\": \"action\" }` reads the `mcp` or `builtin` action that ran (`null` on other types). 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",
        "additionalProperties": false,
        "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": {
            "$ref": "#/components/schemas/ToolExecuteConfig"
          },
          "mcp": {
            "$ref": "#/components/schemas/ToolMcpConfig"
          },
          "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. 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. On an `mcp` tool a preset reaches each action whose input schema declares it; an action whose schema declares no properties receives every one.\n\nValues 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",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "http",
              "client",
              "mcp",
              "builtin",
              "pipeline"
            ]
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "parameters": {
            "type": "object",
            "nullable": true
          },
          "execute": {
            "$ref": "#/components/schemas/ToolExecuteConfig"
          },
          "mcp": {
            "$ref": "#/components/schemas/ToolMcpConfig"
          },
          "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. 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. On an `mcp` tool a preset reaches each action whose input schema declares it; an action whose schema declares no properties receives every one.\n\nValues 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.\n"
          },
          "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.\n",
            "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.\nThis 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`.\n",
            "example": {
              "tenantId": "acme",
              "userToken": "tok_abc123"
            }
          }
        }
      },
      "ToolExecuteConfig": {
        "type": "object",
        "nullable": true,
        "additionalProperties": false,
        "description": "Execution config for http tools. Supported fields: `url` (required), `method` (default `POST`), `headers`, `body_mode` and `response_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). `response_mode` is `json` (default: the body parsed as JSON, else returned as text) or `base64`, which returns a 2xx body as `{ content_type, filename, data_base64 }` — the same file shape `multipart` accepts, so a pipeline step can relay a downloaded file. `filename` comes from `Content-Disposition` and is omitted when the target names none; a body over `TOOL_RESPONSE_MAX_BYTES` is `502 TOOL_RESPONSE_TOO_LARGE`.\n\n`auth` adds a computed request credential, for targets whose `Authorization` value cannot be expressed as a static header. Supported `auth.type` values:\n\n- `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.\n- `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.\n\nCredential 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.\n\nA credential slot takes a reference, never a literal: a literal `secret_access_key`, `session_token` or `credentials` under `auth`, a literal credential-named `headers` value (`Authorization`, `Cookie`, or a name containing `api-key`/`token`/`secret`/`password`), or a `url` carrying a password or a literal credential-named query parameter (those names, plus `key` and `sig`/`signature`) is refused with `400 LITERAL_CREDENTIAL`, `meta.paths` naming each one. A stored literal reads back as `{\"no_echo\": true}` (a `url` holding one is masked whole), an object rather than a string so a read-edit-write round trip fails the schema check instead of writing the placeholder in as the credential. A value carrying a `{{secret:...}}` reference is the wiring, not the credential, and stays readable.\n\n`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.\n",
        "properties": {
          "url": {
            "type": "string",
            "description": "Endpoint URL. May carry `{paramName}` placeholders resolved from the tool arguments at call time."
          },
          "method": {
            "type": "string",
            "nullable": true,
            "description": "HTTP method (default: `POST`)"
          },
          "headers": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "string"
            },
            "description": "Static headers sent on every request. Values accept `{{secret:...}}` and `{{context:<key>}}` references."
          },
          "body_mode": {
            "type": "string",
            "nullable": true,
            "enum": [
              "json",
              "multipart",
              null
            ],
            "description": "Request body encoding. Incompatible with `auth.type: aws_sigv4`."
          },
          "response_mode": {
            "type": "string",
            "nullable": true,
            "enum": [
              "json",
              "base64",
              null
            ],
            "description": "How a 2xx response body is returned. `base64` returns `{ content_type, filename, data_base64 }` for binary targets."
          },
          "auth": {
            "$ref": "#/components/schemas/ToolExecuteAuthConfig"
          }
        }
      },
      "ToolExecuteAuthConfig": {
        "type": "object",
        "nullable": true,
        "additionalProperties": false,
        "description": "Computed request credential, for a target whose `Authorization` value cannot be expressed as a static header. `aws_sigv4` requires `region`, `service`, `access_key_id` and `secret_access_key`; `gcp_service_account` requires `credentials` and `scopes`. Every credential field takes a `{{secret:...}}` reference; a literal is refused with `400 LITERAL_CREDENTIAL`.",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "aws_sigv4",
              "gcp_service_account"
            ],
            "description": "Which credential is computed."
          },
          "region": {
            "type": "string",
            "nullable": true,
            "description": "`aws_sigv4`: the signing region, e.g. `us-east-1`."
          },
          "service": {
            "type": "string",
            "nullable": true,
            "description": "`aws_sigv4`: the signing service, e.g. `execute-api`."
          },
          "access_key_id": {
            "type": "string",
            "nullable": true,
            "description": "`aws_sigv4`: the access key id."
          },
          "secret_access_key": {
            "type": "string",
            "nullable": true,
            "description": "`aws_sigv4`: the secret access key, as a `{{secret:...}}` reference; a literal is refused."
          },
          "session_token": {
            "type": "string",
            "nullable": true,
            "description": "`aws_sigv4`: the session token of a temporary credential, as a reference, like `secret_access_key`."
          },
          "credentials": {
            "type": "string",
            "nullable": true,
            "description": "`gcp_service_account`: the service account key file JSON, as a reference to a secret holding it, like `secret_access_key`."
          },
          "scopes": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "`gcp_service_account`: the OAuth scopes to mint the token for."
          }
        }
      },
      "ToolMcpConfig": {
        "type": "object",
        "nullable": true,
        "additionalProperties": false,
        "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. A literal credential in `headers` or `url` is refused, exactly as in `execute`.",
        "properties": {
          "url": {
            "type": "string",
            "description": "MCP server URL. Accepts a `{{secret:...}}` reference."
          },
          "headers": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "string"
            },
            "description": "Headers sent on every MCP request. Values accept `{{secret:...}}` and `{{context:<key>}}` references."
          }
        }
      },
      "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).\n",
            "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).\n"
          },
          "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.\n"
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why a generation in this trace failed (e.g. an upstream AI provider error). Null if no failure has been recorded."
          },
          "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.\n"
          },
          "content_redacted_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who purged the content: `user` or `api_key` for a purge request, `system` for the retention sweep and for content never stored under zero retention."
          },
          "content_redacted_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal — the API key's own id for key auth, so the record names which key acted. `retention_sweep` or `zero_retention` for `system`.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "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": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why 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.\n"
          },
          "content_redacted_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who purged the content: `user` or `api_key` for a purge request, `system` for the retention sweep and for content never stored under zero retention."
          },
          "content_redacted_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal — the API key's own id for key auth, so the record names which key acted. `retention_sweep` or `zero_retention` for `system`.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "children": {
            "type": "array",
            "description": "Child traces triggered by sub-agent calls from this trace",
            "items": {
              "$ref": "#/components/schemas/TraceTreeNode"
            }
          }
        }
      },
      "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"
        ],
        "additionalProperties": false,
        "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"
          },
          "tool_context": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Caller context every firing forwards to the run it starts, so an agent whose tools authorize through `{{context:<key>}}` can be put on a schedule — a firing has no request to carry one. Each key is forwarded as one `X-Soat-Context-<key>` header.\n\n**Write-only.** It is accepted here and never returned on a read, so the record cannot be used to recover a value.\n\nA value may be a `{{secret:sec_...}}` reference, which keeps the credential in the [secret](/docs/modules/secrets) store and leaves only its id on the trigger; it is resolved at fire time, so rotating the secret changes the next firing without touching the trigger. A reference is refused here rather than at fire time when it is not in that id form (a secret's name does not resolve) or names a secret that does not exist in this project.\n"
          },
          "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",
        "additionalProperties": false,
        "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
          },
          "tool_context": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "string"
            },
            "description": "Replaces the stored bag; `null` clears it. Write-only and resolved at fire time — see `CreateTriggerRequest.tool_context`.\n"
          },
          "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",
        "additionalProperties": false,
        "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`."
          },
          "tool_context": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Fire-time caller context, shallow-merged per key over the trigger's stored `tool_context`. A manual fire has a caller, so this is the one path that can supply a value without storing it.\n\nForwarded exactly as written: a `{{secret:...}}` reference is resolved only in the trigger's stored bag, never in one supplied here.\n"
          }
        }
      },
      "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": [
              "queued",
              "running",
              "completed",
              "failed"
            ]
          },
          "input": {
            "type": "object",
            "nullable": true,
            "description": "Effective input, after the trigger's static input is merged under the fire-time input. For an `event` firing the fire-time input is the event envelope a webhook delivery carries (`id`, `event`, `project_id`, `resource_type`, `resource_id`, `data`, `timestamp`)."
          },
          "result": {
            "type": "object",
            "nullable": true,
            "description": "{ target_type, result_id, status, output } — output truncated"
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why the firing failed, when it did."
          },
          "idempotency_key": {
            "type": "string",
            "nullable": true,
            "description": "`<event_id>:<trigger_id>` for an `event` firing, where `event_id` is the `id` in `input`; null for every other source. The same event reaching the same trigger twice produces one firing under this key, so a consumer reading firings can recognise a redelivery."
          },
          "attempts": {
            "type": "integer",
            "description": "Dispatch attempts started. Above 1 means an earlier attempt was interrupted before it recorded a result and the firing was redelivered — not that the target refused it, which is recorded terminally in `error` and never retried."
          },
          "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` for prompt-cache reads, `cache_write_tokens` for prompt-cache writes, and a non-billable `reasoning_tokens` detail), a `compute_execution` event has one `compute_second` component.\n\nThe three input dimensions are disjoint and partition the provider's reported prompt tokens: `input_tokens` is the uncached input **alone**. They are separate components because they are separately priced — a cache read costs a fraction of an uncached token and a cache write costs more than one. A dimension with no price row of its own falls back to the `input_tokens` rate, never to zero.\n",
        "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.\n"
          },
          "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 whole generation, a node execution, an embedding call. A generation is one event however many steps it took — a four-step turn is four LLM calls and one event, written when the turn ends. Attribution and total cost live here; the measured quantities live in `components`.\n",
        "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.\n"
          },
          "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.\n"
          },
          "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.\n"
          },
          "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. Kept after the provider is deleted, like every attribution id on the event.\n"
          },
          "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.\n"
          },
          "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.\n"
          },
          "tool_id": {
            "x-soat-ref": "tools",
            "type": "string",
            "nullable": true,
            "description": "Public ID of the tool a `tool_execution` event metered. Null on every other meter and for an inline tool definition.\n"
          },
          "publisher_project_id": {
            "x-soat-ref": "projects",
            "type": "string",
            "nullable": true,
            "description": "The project that owns the resource this event metered, when the call reached it through a share. The event itself belongs to the calling project. Null for a call on the project's own resources.\n"
          },
          "document_id": {
            "x-soat-ref": "documents",
            "type": "string",
            "nullable": true,
            "description": "Public ID of the document an `embedding` event embedded a chunk of — ingestion, a re-chunk, a conversation's turns. Null on a query embedding and on every other meter.\n"
          },
          "memory_store_id": {
            "x-soat-ref": "memory-stores",
            "type": "string",
            "nullable": true,
            "description": "Public ID of the memory store an `embedding` event embedded written content for. The store rather than the memory: stored content is shared by every memory in the store that states it. Null on a query embedding and on every other meter.\n"
          },
          "outcome": {
            "type": "string",
            "nullable": true,
            "description": "How a `tool_execution` call that went out ended, whatever the target answered: `ok`, `error` (including a non-2xx answer), or `timeout`. Null on every other meter.\n"
          },
          "meter_type": {
            "type": "string",
            "description": "What the event measures: `llm_tokens`, `compute_execution`, `api_request`, `storage`, or `tool_execution` — one outbound tool call, a single `tool_call` component of quantity 1, unpriced.\n"
          },
          "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, and `eval_scorer` a `tool` scorer's call, so verification spend is `source` in (`eval`, `eval_judge`, `eval_scorer`) and they are distinct so running a suite can be priced apart from grading it. `decider` is a decision's evaluation. `embedding` is any embedding call — the endpoint, document ingestion, a memory store write, or a search's query vector. Null for ordinary agent traffic.\n",
            "example": "eval"
          },
          "provider": {
            "type": "string",
            "description": "As-billed SKU vendor slug, retained even if the AI provider is deleted. `soat` for platform meter types.\n",
            "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.\n"
          },
          "components": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UsageComponent"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "UsageGroupBy": {
        "type": "string",
        "description": "The dimension an aggregate buckets on. `orchestration_run` buckets on the orchestration run; work that runs no orchestration collapses into the single `null` bucket. `tool` buckets `tool_execution` events on the tool they metered; every other meter is the `null` bucket.\n",
        "enum": [
          "model",
          "ai_provider",
          "agent",
          "orchestration_run",
          "day",
          "meter_type",
          "actor",
          "session",
          "source",
          "tool"
        ]
      },
      "UsageDistinct": {
        "type": "object",
        "description": "How many distinct entities of each kind the window touched — the counters a \"how many generations / runs / actors this cycle\" question reads. Present on `totals` exactly when `include=distinct` was sent, and never on a group — filling it per bucket would mean a `COUNT(DISTINCT)` per bucket on the page query.\n\nNulls are not counted: a generation-less completion (`chat`, `memory_extraction`, `memory_consolidation`, `eval_judge`) contributes to `event_count` and to nothing here; a standalone generation contributes to `generations` and not to `orchestration_runs`; a retried orchestration node — a second event and a second generation — counts once as a run.\n\nEvery figure describes one window and none of them add. Two adjacent windows' `sessions` overlap wherever a session spans the boundary, and a run that straddles midnight is in two `day` windows. A wider figure is a wider query, never a sum of narrower ones.\n",
        "properties": {
          "generations": {
            "type": "integer"
          },
          "traces": {
            "type": "integer"
          },
          "orchestration_runs": {
            "type": "integer"
          },
          "agents": {
            "type": "integer"
          },
          "actors": {
            "type": "integer"
          },
          "sessions": {
            "type": "integer"
          },
          "ai_providers": {
            "type": "integer"
          },
          "tools": {
            "type": "integer"
          },
          "documents": {
            "type": "integer"
          },
          "memory_stores": {
            "type": "integer"
          },
          "publisher_projects": {
            "type": "integer"
          }
        }
      },
      "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.\n",
        "properties": {
          "component": {
            "type": "string",
            "description": "The measured dimension: `input_tokens`, `cached_tokens`, `cache_write_tokens`, `output_tokens`, `reasoning_tokens`, `compute_second`, `request`, `gb_day`, `chunk_count`, `record_gb_day`, …\n"
          },
          "unit": {
            "type": "string",
            "description": "Unit `quantity` is measured in: `token`, `compute_second`, `request`, `gb_day`, `count`.\n"
          },
          "quantity": {
            "type": "number",
            "description": "Summed measured amount in `unit`. Non-integer for fractional measures (GB-days, compute seconds).\n",
            "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.\n"
          }
        }
      },
      "UsageAggregateBucket": {
        "allOf": [
          {
            "$ref": "#/components/schemas/UsageTotals"
          },
          {
            "type": "object",
            "description": "Summed token counts, measured component quantities, and cost for a bucket (or the grand total).\n",
            "properties": {
              "event_count": {
                "type": "integer",
                "description": "Usage events measured in the bucket. On `totals` this counts the whole `[from, to]` window.\n"
              },
              "components": {
                "type": "array",
                "description": "Every component measured in the bucket, sorted by `component` then `unit`. The token fields cover `llm_tokens` only, so this is where a `storage` / `api_request` / `compute_execution` bucket reports what it actually measured.\n",
                "items": {
                  "$ref": "#/components/schemas/UsageAggregateComponent"
                }
              }
            }
          }
        ]
      },
      "UsageAggregateTotals": {
        "allOf": [
          {
            "$ref": "#/components/schemas/UsageAggregateBucket"
          },
          {
            "type": "object",
            "properties": {
              "distinct": {
                "$ref": "#/components/schemas/UsageDistinct"
              }
            }
          }
        ]
      },
      "UsageAggregateFilters": {
        "type": "object",
        "description": "Every narrowing the rollup applied, echoed back exactly as the caller sent it and null when unset. Echoed because a rollup of zeros is otherwise indistinguishable from a project that spent nothing, and always complete so a caller reading one key can tell \"not filtered\" from \"this endpoint does not know that filter\". The ten naming a resource are resolved against the project and empty the rollup when they name nothing in it; the other six are matched against the value the event recorded.\n",
        "properties": {
          "meter_type": {
            "type": "string",
            "nullable": true
          },
          "model": {
            "type": "string",
            "nullable": true
          },
          "source": {
            "type": "string",
            "nullable": true
          },
          "trigger_id": {
            "type": "string",
            "nullable": true
          },
          "action_id": {
            "type": "string",
            "nullable": true
          },
          "session_id": {
            "x-soat-ref": "sessions",
            "type": "string",
            "nullable": true
          },
          "actor_id": {
            "x-soat-ref": "actors",
            "type": "string",
            "nullable": true
          },
          "agent_id": {
            "x-soat-ref": "agents",
            "type": "string",
            "nullable": true
          },
          "ai_provider_id": {
            "x-soat-ref": "ai-providers",
            "type": "string",
            "nullable": true
          },
          "orchestration_run_id": {
            "x-soat-ref": "orchestration-runs",
            "type": "string",
            "nullable": true
          },
          "orchestration_id": {
            "x-soat-ref": "orchestrations",
            "type": "string",
            "nullable": true
          },
          "generation_id": {
            "x-soat-ref": "generations",
            "type": "string",
            "nullable": true
          },
          "trace_id": {
            "x-soat-ref": "traces",
            "type": "string",
            "nullable": true
          },
          "tool_id": {
            "x-soat-ref": "tools",
            "type": "string",
            "nullable": true
          },
          "publisher_project_id": {
            "x-soat-ref": "projects",
            "type": "string",
            "nullable": true
          },
          "outcome": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "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": {
            "allOf": [
              {
                "$ref": "#/components/schemas/UsageGroupBy"
              }
            ],
            "nullable": true,
            "description": "Dimension applied, echoed back; null when the caller asked for no bucketing, in which case `groups` is an empty page.\n"
          },
          "filters": {
            "$ref": "#/components/schemas/UsageAggregateFilters"
          },
          "groups": {
            "type": "object",
            "description": "One entry per distinct value in the chosen dimension, paginated. Ordered by `cost_usd` descending, ties broken by `key` then `ai_provider_id` ascending, with nulls last — so the first page is the biggest spenders and paging never repeats or skips a bucket.\n",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/UsageAggregateGroup"
                }
              },
              "total": {
                "type": "integer",
                "description": "Distinct buckets in the window, independent of `limit`, including a null bucket for the events the dimension does not apply to. Bucket cardinality is not an entity count on any dimension that admits nulls: a project whose traffic is standalone generations has exactly one `group_by=orchestration_run` bucket whatever its volume. Read `totals.distinct` (under `include=distinct`) to count entities.\n"
              },
              "limit": {
                "type": "integer"
              },
              "offset": {
                "type": "integer"
              }
            }
          },
          "totals": {
            "allOf": [
              {
                "$ref": "#/components/schemas/UsageAggregateTotals"
              },
              {
                "description": "The whole `[from, to]` window, never the `groups` page above it.\n"
              }
            ]
          }
        }
      },
      "UsageAggregateGroup": {
        "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 / orchestration run / actor / session / tool 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, and under `orchestration_run` the one holding every generation that ran no orchestration.\n"
              },
              "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`.\n"
              }
            }
          },
          {
            "$ref": "#/components/schemas/UsageAggregateBucket"
          }
        ]
      },
      "UsageThreshold": {
        "type": "object",
        "description": "A per-project alert rule on windowed usage. When the project's `metric` over `window` crosses `threshold`, a `usage_thresholds.crossed` webhook fires. Fire state is enforced with once-per-window (calendar) / 10% re-arm (rolling) hysteresis.\n",
        "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).\n"
          },
          "window": {
            "type": "string",
            "enum": [
              "calendar_month",
              "rolling_24h"
            ],
            "description": "The evaluation window: the current UTC calendar month, or the trailing 24 hours.\n"
          },
          "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.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateUsageThresholdRequest": {
        "type": "object",
        "required": [
          "project_id",
          "metric",
          "window",
          "threshold"
        ],
        "additionalProperties": false,
        "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 an orchestration-run receipt.\n",
            "x-soat-ref": "generations",
            "type": "string"
          },
          "orchestration_run_id": {
            "description": "Present on an orchestration-run receipt (summed across the run's events); absent on a per-generation receipt.\n",
            "x-soat-ref": "orchestration-runs",
            "type": "string"
          },
          "line_items": {
            "type": "array",
            "description": "One line per usage event — for a generation receipt, the events on that generation; for an orchestration-run receipt, every event across the run.\n",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "The usage event's ID",
                  "example": "ue_V1StGXR8Z5jdHi6B"
                },
                "meter_type": {
                  "type": "string"
                },
                "provider": {
                  "type": "string"
                },
                "model": {
                  "type": "string"
                },
                "node_id": {
                  "type": "string",
                  "nullable": true,
                  "description": "Orchestration node that produced the event. On an orchestration-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).\n"
                },
                "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.\n",
            "items": {
              "type": "object",
              "properties": {
                "meter_type": {
                  "type": "string"
                },
                "cost_usd": {
                  "type": "number",
                  "nullable": true
                }
              }
            }
          },
          "totals": {
            "$ref": "#/components/schemas/UsageTotals"
          }
        }
      },
      "Price": {
        "type": "object",
        "description": "A versioned unit price for one billable component of a SKU. Cost is uniform across meter types — quantity × unit_price.\n",
        "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.\n"
          },
          "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)"
          },
          "resource": {
            "type": "string",
            "nullable": true,
            "description": "The tool or agent SRN a resource row prices; null for a SKU row"
          },
          "quantity": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "A resource row's quantity expression; null prices one per call"
          },
          "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"
        ],
        "additionalProperties": false,
        "properties": {
          "prices": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "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` and `cache_write_tokens`) components with unit `token`; for platform SKUs supply the platform component (e.g. `compute_second`).\n",
              "additionalProperties": false,
              "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.\n"
                },
                "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.\n"
                },
                "resource": {
                  "type": "string",
                  "description": "The SRN of one tool or agent, `srn:<project_id>:<tool|agent>:<id>`, for a resource row; omit `provider` and `model`. A tool's row prices its `tool_execution` events, beside `tool_call`; an agent's prices its `llm_tokens` events, beside the provider cost. A client tool has no execution of its own and cannot be priced.",
                  "example": "srn:proj_V1StGXR8Z5jdHi6B:tool:tool_V1StGXR8Z5jdHi6B"
                },
                "quantity": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "On a resource row only: JSON Logic giving the quantity `unit_price` multiplies, over `{ input, action, response, outcome, duration_ms }` for a tool and `{ response: { usage, cost_usd, steps, tool_calls, stop_reason }, outcome }` for an agent. Omit to price one per call. A result that is not a finite number >= 0 records quantity `0` and no cost.",
                  "example": {
                    "var": "response.page_count"
                  }
                },
                "provider": {
                  "type": "string",
                  "description": "The SKU vendor; omit on a resource row"
                },
                "model": {
                  "type": "string",
                  "description": "The SKU; omit on a resource row"
                },
                "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"
                }
              }
            }
          }
        }
      },
      "UsageTotals": {
        "type": "object",
        "description": "Token counts and cost for one slice of the meter. Tokens and cost only: a `compute_second` or `gb_day` meter is reported by the aggregate's `components` array.\n\nThe three input dimensions partition the prompt — `input_tokens` = `uncached_input_tokens` + `cached_tokens` + `cache_write_tokens` — and are separate because they are separately priced. Note that the `input_tokens` **component** on a usage event is the uncached figure alone; here `input_tokens` is the whole prompt.\n",
        "properties": {
          "cost_usd": {
            "type": "number",
            "nullable": true,
            "description": "Sum of priced component costs; null when nothing in the slice was priced. Always null at step altitude — the ledger prices one event per generation at write time, and a price read back per step would disagree with it after any price change."
          },
          "input_tokens": {
            "type": "integer",
            "description": "Full prompt tokens, reconstructed from the components."
          },
          "uncached_input_tokens": {
            "type": "integer",
            "description": "Prompt tokens priced at the plain input rate — the prompt minus cache reads and cache writes."
          },
          "output_tokens": {
            "type": "integer"
          },
          "cached_tokens": {
            "type": "integer",
            "description": "Prompt tokens served from the provider's prompt cache."
          },
          "cache_write_tokens": {
            "type": "integer",
            "description": "Prompt tokens written into the provider's prompt cache."
          },
          "reasoning_tokens": {
            "type": "integer",
            "description": "A non-billable subset of `output_tokens`, reported where the provider breaks it out."
          }
        }
      },
      "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"
        ],
        "additionalProperties": false,
        "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",
        "additionalProperties": false,
        "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"
          }
        }
      },
      "EventEnvelope": {
        "type": "object",
        "description": "The body of a webhook delivery, and the input of an `event` trigger\nfiring. Every delivery and redelivery of one event carries the same\n`id`, also sent as the `X-Soat-Event-Id` header, so a subscriber\ndedupes on it.\n",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's identity, shared by every delivery of it"
          },
          "event": {
            "type": "string",
            "description": "Event type, e.g. `files.created`"
          },
          "project_id": {
            "x-soat-ref": "projects",
            "type": "string",
            "description": "Public ID of the project the event belongs to"
          },
          "resource_type": {
            "type": "string",
            "description": "Type of the resource that changed, e.g. `file`"
          },
          "resource_id": {
            "type": "string",
            "description": "Public ID of the resource that changed"
          },
          "data": {
            "type": "object",
            "description": "The resource, in the shape the REST API returns it"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "When the event occurred"
          }
        }
      },
      "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": {
            "$ref": "#/components/schemas/EventEnvelope"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "completed",
              "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\nthe delivery is queued and a retry is still owed; null once it has\ncompleted or exhausted its attempts.\n"
          },
          "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.",
        "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,
            "description": "What entering the state dispatches, and how the result routes. Omitted or null on a state that only parks.",
            "properties": {
              "dispatch": {
                "type": "object",
                "description": "The one thing entering the state runs. `kind` picks it and names the id it needs: `agent` → `agent_id`, `orchestration` → `orchestration_id`, `tool` → `tool_id` (with an optional `operation_id`).",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "agent",
                      "orchestration",
                      "tool"
                    ],
                    "description": "Which target is dispatched."
                  },
                  "agent_id": {
                    "x-soat-ref": "agents",
                    "type": "string",
                    "nullable": true,
                    "description": "The agent a `kind: agent` dispatch generates with."
                  },
                  "orchestration_id": {
                    "x-soat-ref": "orchestrations",
                    "type": "string",
                    "nullable": true,
                    "description": "The orchestration a `kind: orchestration` dispatch starts a run of."
                  },
                  "tool_id": {
                    "x-soat-ref": "tools",
                    "type": "string",
                    "nullable": true,
                    "description": "The tool a `kind: tool` dispatch calls."
                  },
                  "operation_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "Which operation of a multi-operation tool to invoke — the same selector an orchestration `tool` node takes."
                  },
                  "input_mapping": {
                    "type": "object",
                    "nullable": true,
                    "additionalProperties": true,
                    "description": "JSON Logic building the dispatch input from the task. Carried verbatim."
                  },
                  "payload_writes": {
                    "type": "object",
                    "nullable": true,
                    "additionalProperties": true,
                    "description": "JSON Logic expressions over `{task, result}` whose values are written into `task.payload` when the dispatch completes, one named channel per key. Carried verbatim."
                  }
                }
              },
              "retry": {
                "type": "object",
                "nullable": true,
                "description": "Re-runs the dispatch's execution failures before `on_failure` applies. Omitted means a single attempt.",
                "properties": {
                  "max_attempts": {
                    "type": "integer",
                    "description": "Attempts to make, 1–10."
                  },
                  "backoff_seconds": {
                    "type": "number",
                    "nullable": true,
                    "description": "Delay before the second attempt."
                  },
                  "backoff_multiplier": {
                    "type": "number",
                    "nullable": true,
                    "description": "Factor the delay grows by per further attempt: `backoff_seconds * backoff_multiplier^(n - 2)`."
                  }
                }
              },
              "on_complete": {
                "type": "array",
                "nullable": true,
                "description": "Rules evaluated in order when the dispatch completes; the first whose `when` is truthy fires its transition.",
                "items": {
                  "type": "object",
                  "properties": {
                    "when": {
                      "description": "JSON Logic over `{task, result}`. Carried verbatim."
                    },
                    "transition": {
                      "type": "string",
                      "description": "The transition to fire, by name."
                    }
                  }
                }
              },
              "on_failure": {
                "type": "string",
                "nullable": true,
                "description": "Transition fired when the dispatch fails after its last attempt. Omitted parks the task with `automation_status: failed`."
              }
            }
          }
        }
      },
      "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.",
        "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.\n",
            "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"
        ],
        "additionalProperties": false,
        "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",
        "additionalProperties": false,
        "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"
          },
          "expected_version": {
            "description": "Refuses the write unless the resource is at this version.",
            "allOf": [
              {
                "$ref": "#/components/schemas/ExpectedVersion"
              }
            ]
          }
        }
      },
      "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.\n\nDeliberately 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_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who produced this version. Null for a write with no principal behind it."
          },
          "created_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of that principal: the user (`user_…`), or for API key auth the key itself (`key_…`), so the record names which key acted.",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateWorkflowMigrationRequest": {
        "type": "object",
        "required": [
          "to_version"
        ],
        "additionalProperties": false,
        "properties": {
          "to_version": {
            "type": "integer",
            "minimum": 1,
            "description": "The workflow version to move the open tasks onto."
          },
          "state_map": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Current state name to a state the target version declares, applied to every task as migrate-task applies it to one."
          },
          "wait": {
            "type": "boolean",
            "default": false,
            "description": "When omitted or `false` (default), the run is answered `queued` and settles in the background; poll get-workflow-migration. Pass `true` to answer once every task has settled."
          }
        }
      },
      "WorkflowMigrationTask": {
        "type": "object",
        "required": [
          "task_id",
          "status",
          "error"
        ],
        "properties": {
          "task_id": {
            "type": "string",
            "example": "task_V1StGXR8Z5jdHi6B"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "migrated",
              "refused"
            ]
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoredError"
              }
            ],
            "nullable": true,
            "description": "Why the task was refused: `{ code, message, meta? }`; null otherwise"
          }
        }
      },
      "WorkflowMigration": {
        "type": "object",
        "required": [
          "id",
          "workflow_id",
          "project_id",
          "to_version",
          "state_map",
          "status",
          "task_count",
          "migrated_count",
          "refused_count",
          "tasks",
          "created_by_principal_type",
          "created_by_principal_id",
          "created_at",
          "finished_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "wfl_mig_V1StGXR8Z5jdHi6B"
          },
          "workflow_id": {
            "type": "string",
            "example": "wfl_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "to_version": {
            "type": "integer"
          },
          "state_map": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "string"
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "completed"
            ],
            "description": "`completed` once every task is `migrated` or `refused`."
          },
          "task_count": {
            "type": "integer"
          },
          "migrated_count": {
            "type": "integer"
          },
          "refused_count": {
            "type": "integer"
          },
          "tasks": {
            "type": "array",
            "description": "Every task the run covers, in the order it migrates them.",
            "items": {
              "$ref": "#/components/schemas/WorkflowMigrationTask"
            }
          },
          "created_by_principal_type": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PrincipalType"
              }
            ],
            "description": "Who started the migration."
          },
          "created_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of who started the migration."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the last task settled; null until then."
          }
        }
      },
      "RestoreWorkflowVersionRequest": {
        "type": "object",
        "additionalProperties": false,
        "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": {
      "AgentId": {
        "name": "agent_id",
        "in": "path",
        "required": true,
        "description": "Agent public ID",
        "schema": {
          "type": "string",
          "example": "agent_V1StGXR8Z5jdHi6B"
        }
      },
      "GenerationId": {
        "name": "generation_id",
        "in": "path",
        "required": true,
        "description": "Public ID of the generation paused at `requires_action`",
        "schema": {
          "type": "string",
          "example": "gen_V1StGXR8Z5jdHi6B"
        }
      },
      "AgentVersionNumber": {
        "name": "version",
        "in": "path",
        "required": true,
        "description": "Archived config version number",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "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"
        }
      },
      "IfMatchVersion": {
        "name": "If-Match",
        "in": "header",
        "required": false,
        "description": "The version the caller believes the resource holds, as an entity tag (`3` or `\"3\"`). Equivalent to `expected_version` in the request body; `*` states no precondition beyond the resource existing. A mismatch is `409 VERSION_CONFLICT`.",
        "schema": {
          "type": "string"
        },
        "example": "3"
      },
      "DeciderId": {
        "name": "decider_id",
        "in": "path",
        "required": true,
        "description": "The decider ID",
        "schema": {
          "type": "string",
          "example": "dcd_V1StGXR8Z5jdHi6B"
        }
      },
      "Version": {
        "name": "version",
        "in": "path",
        "required": true,
        "description": "The archived version number",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "exception_id": {
        "name": "exception_id",
        "in": "path",
        "required": true,
        "description": "Exception item ID",
        "schema": {
          "type": "string",
          "example": "exc_V1StGXR8Z5jdHi6B"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "The request's deduplication key. Equivalent to `idempotency_key` in the request body; sending both with different values is `400 VALIDATION_FAILED`. A non-empty string of at most 255 characters.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255
        },
        "example": "discord-msg-1234567890"
      },
      "MetadataQuery": {
        "name": "metadata",
        "in": "query",
        "required": false,
        "description": "A `MetadataFilter` as JSON, url-encoded. It travels as JSON rather than as `key:value` pairs because the match is exact and a query string cannot otherwise say whether `3` is the number or the string.",
        "schema": {
          "type": "string"
        },
        "example": "{\"quarter\":\"Q1\",\"revision\":{\"gte\":3}}"
      },
      "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_...)"
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Results per page. Above the maximum it is clamped, and `limit` in the response reports the page served; below 1 or not an integer it is `400 VALIDATION_FAILED`.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 50
        }
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "required": false,
        "description": "Results to skip. Negative or not an integer it is `400 VALIDATION_FAILED`.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      },
      "PlaybookId": {
        "name": "playbook_id",
        "in": "path",
        "required": true,
        "description": "The playbook ID",
        "schema": {
          "type": "string",
          "example": "pbk_V1StGXR8Z5jdHi6B"
        }
      },
      "CaseId": {
        "name": "case_id",
        "in": "path",
        "required": true,
        "description": "The case ID",
        "schema": {
          "type": "string",
          "example": "pbk_case_V1StGXR8Z5jdHi6B"
        }
      },
      "NodeKey": {
        "name": "node",
        "in": "path",
        "required": true,
        "description": "The ID of a node of the case's playbook version",
        "schema": {
          "type": "string",
          "example": "revenue"
        }
      },
      "ExecutionId": {
        "name": "execution_id",
        "in": "path",
        "required": true,
        "description": "The execution ID",
        "schema": {
          "type": "string",
          "example": "pbk_exe_V1StGXR8Z5jdHi6B"
        }
      },
      "ExportId": {
        "name": "export_id",
        "in": "path",
        "required": true,
        "description": "The export ID",
        "schema": {
          "type": "string",
          "example": "pbk_exp_V1StGXR8Z5jdHi6B"
        }
      },
      "SessionId": {
        "name": "session_id",
        "in": "path",
        "required": true,
        "description": "Session public ID",
        "schema": {
          "type": "string",
          "example": "sess_V1StGXR8Z5jdHi6B"
        }
      },
      "share_id": {
        "name": "share_id",
        "in": "path",
        "required": true,
        "description": "Share ID",
        "schema": {
          "type": "string",
          "example": "shr_V1StGXR8Z5jdHi6B"
        }
      },
      "acceptance_id": {
        "name": "acceptance_id",
        "in": "path",
        "required": true,
        "description": "Share acceptance ID",
        "schema": {
          "type": "string",
          "example": "shr_acc_V1StGXR8Z5jdHi6B"
        }
      },
      "TagsQuery": {
        "name": "tags",
        "in": "query",
        "required": false,
        "description": "Filter by tag pairs, written `key:value` (split on the first colon, so a value may contain colons). Repeat the parameter for several pairs; **all** must be present with exactly that value.\n",
        "schema": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "style": "form",
        "explode": true,
        "example": [
          "env:prod"
        ]
      }
    },
    "responses": {
      "VersionConflict": {
        "description": "The resource has moved past the version this write read (`VERSION_CONFLICT`): a stated `expected_version` or `If-Match` no longer matches, or a concurrent write took the version first. `meta.current_version` names the version in force. Nothing is written.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Authentication required",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Insufficient permissions",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "FORBIDDEN",
                "message": "Forbidden",
                "hint": "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"
              }
            }
          }
        }
      },
      "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, traces, deciders, accepted shares or ingestion rules and cannot be deleted. `meta.decider_count` names the deciders, which block deletion even under `force=true` and must be deleted or repointed first; generations, traces, accepted shares (`meta.accepted_share_count`) and its own project's ingestion rules (`meta.ingestion_rule_count`) block only until `force=true`, which revokes the shares and leaves the rules naming a converter that is gone.",
      "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, a `bedrock`/`vertex` record that links no credential at all on a deployment that does not let a record use the deployment's own, or one whose linked secret holds no usable credential.",
      "resolution": "The provider record is missing something it needs to make a call — usually the API key secret or the base URL. A `bedrock` or `vertex` record linking no credential is this too: it would sign with the deployment's own, which only an operator may allow. 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, and must satisfy the tool's own `parameters` 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#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"
    },
    "CONVERSATION_POSITION_CONFLICT": {
      "http_status": 409,
      "description": "Another write took the message's `position` in the conversation at the same time. Nothing was written: retry the write.",
      "resolution": "Retry the write unchanged: nothing was stored, and the retry resolves its position again.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#conversation_position_conflict"
    },
    "CONVERSION_TIMEOUT": {
      "http_status": 500,
      "description": "Recorded on a document, never returned by a request: its converter deferred with `{ status: \"pending\" }` and did not call back within `CONVERSION_STALL_TIMEOUT_MS` (thirty minutes by default), so the next read marked it failed.",
      "resolution": "Check that the converter calls back to the document's callback URL, then ingest the document again. Raise `CONVERSION_STALL_TIMEOUT_MS` for a converter that legitimately takes longer.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#conversion_timeout"
    },
    "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"
    },
    "DECIDER_AGENT_NOT_TOOL_LESS": {
      "http_status": 400,
      "description": "The agent a decider evaluates with has a tool surface: a tool binding left active by `active_tool_ids`, or a `knowledge_config.write_memory_store_id`. A decision has no resume route, and a tool call during evaluation would take a side effect the decision cannot record, so a decider agent carries no tools. Knowledge retrieval stays available.",
      "resolution": "Point the decider at an agent with no `tool_bindings` and no `knowledge_config.write_memory_store_id`, or remove them from this one with `PATCH /api/v1/agents/{agent_id}`. The agent's `instructions` and knowledge retrieval remain the place for domain context.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#decider_agent_not_tool_less"
    },
    "DECIDER_TOOL_NOT_CALLABLE": {
      "http_status": 400,
      "description": "The tool a decider evaluates with cannot answer a decision. Only `http` and `pipeline` tools can: a `client` tool has no caller to hand the call to, and an `mcp` or `builtin` tool needs an `action` a decider does not carry. A tool whose `preset_parameters` pin `input` or `questions` is refused too, since presets are merged over the call input and would replace the caller's input or the questions. `meta.tool_id` names the tool, with `meta.type` or `meta.pinned`.",
      "resolution": "Point the decider at an `http` or `pipeline` tool with no `input` or `questions` in its `preset_parameters`. Reach an `mcp` or `builtin` tool through a `pipeline` step, which names its own `action`.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#decider_tool_not_callable"
    },
    "DECISION_ANSWER_INVALID": {
      "http_status": 502,
      "description": "Recorded on a decision, never returned by a request: the tool answered outside the contract — not a JSON object with an `answers` array, an answer missing, repeated or naming no question, a value outside the question's answer space, a probability or confidence outside 0–1, or a field the contract does not define. The message names the answer and the field.",
      "resolution": "Make the tool answer `{ answers: [{ name, probability | choice | score, probabilities?, confidence? }] }`, one per question, as OpenAI's Decisions API does; map an engine's own fields with the tool's `output_mapping` or a `pipeline` tool's `output`. Then request a new decision.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#decision_answer_invalid"
    },
    "DECISION_INTERRUPTED": {
      "http_status": 500,
      "description": "Recorded on a decision, never returned by a request: the process evaluating it stopped before it settled. The evaluated input is not stored, so the decision cannot be re-run; request a new one.",
      "resolution": "Request a new decision with `POST /api/v1/deciders/{decider_id}/decisions` or `POST /api/v1/decisions`; nothing about the interrupted one can be resumed.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#decision_interrupted"
    },
    "DOCUMENT_ALREADY_WITHDRAWN": {
      "http_status": 409,
      "description": "The document is already withdrawn, so there is nothing to take out. Restore an earlier version to bring it back.",
      "resolution": "The document is already out of every default read. To bring it back, restore the version before the withdrawal.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#document_already_withdrawn"
    },
    "DOCUMENT_RELATION_EXISTS": {
      "http_status": 409,
      "description": "That relation is already asserted between the two documents. Re-asserting the same edge is the same fact, so there is nothing to add.",
      "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#document_relation_exists"
    },
    "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"
    },
    "EVAL_SCORING_FAILED": {
      "http_status": 500,
      "description": "Recorded on an eval result, never returned by a request: a scorer could not grade the item's output for a reason no more specific code names. The item is excluded from the aggregates rather than scored 0.",
      "resolution": "Read the message for the scorer's failure, fix the scorer's configuration, and run the eval again.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#eval_scoring_failed"
    },
    "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"
    },
    "EXPRESSION_FAILED": {
      "http_status": 422,
      "description": "A JSON Logic expression failed to evaluate: it names an operator the engine does not know, or it ran a `throw`. The message carries what the expression threw.",
      "resolution": "Fix the JSON Logic expression the message names: an unknown operator, or a `throw` the expression reached with the data it was given.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#expression_failed"
    },
    "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"
    },
    "FORMATION_RESOURCE_TYPE_UNREGISTERED": {
      "http_status": 409,
      "description": "The formation holds a resource whose type the deployment no longer registers, so the platform has no handler to act on it. `meta.resource_type` names the type.",
      "resolution": "Register the resource type again so its handler can act on the resource, or remove the resource outside the formation.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#formation_resource_type_unregistered"
    },
    "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_AWAITING_TOOL_OUTPUTS": {
      "http_status": 409,
      "description": "The generation is not paused on client tool calls: it never paused, or its outputs were already submitted and it resumed. Tool outputs are accepted once per pause.",
      "resolution": "Read the generation for its current status. A generation that pauses again answers `requires_action` with new tool call ids; submit outputs for those.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#generation_not_awaiting_tool_outputs"
    },
    "GENERATION_NOT_COMPLETED": {
      "http_status": 409,
      "description": "The generation has not completed, so it has no finished turn: it cannot be curated into a dataset item, and an eval item whose generation settled in another status (a `requires_action` pause, for one) is recorded with this code rather than scored. `meta.status` is the status it settled in.",
      "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 accessible to the caller.",
      "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"
    },
    "IDEMPOTENCY_KEY_REUSED": {
      "http_status": 409,
      "description": "The idempotency key is already claimed by a run, generation, session message or task transition sent from a different request. A key names one request, so a caller that changed the body and kept the key has a bug; send the new request under a new key, or repeat the original body to replay what the key already names.",
      "resolution": "Send the changed request under a new `idempotency_key`; repeating the original body under this key replays what it already names.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#idempotency_key_reused"
    },
    "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_FAILED": {
      "http_status": 500,
      "description": "Recorded on a document, never returned by a request: its ingestion failed for a reason no more specific code names. The message carries the failure.",
      "resolution": "Read the message for the cause, fix the file or its ingestion rule, and ingest the document again.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#ingestion_failed"
    },
    "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"
    },
    "INGESTION_TIMEOUT": {
      "http_status": 500,
      "description": "Recorded on a document, never returned by a request: its ingestion made no progress within `INGESTION_STALL_TIMEOUT_MS` (five minutes by default), so the next read marked it failed.",
      "resolution": "Ingest the document again. A large file or a busy worker can outlast the window; raise `INGESTION_STALL_TIMEOUT_MS` when it does so repeatedly.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#ingestion_timeout"
    },
    "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 SOAT_HEADER_PREFIX followed by Context- (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"
    },
    "LITERAL_CREDENTIAL": {
      "http_status": 400,
      "description": "A tool definition carries a credential as a literal value: a credential-named header, a secret field under execute.auth, or a credential-named query parameter in a url. Store it as a secret and write a {{secret:sec_...}} reference (or a {{context:<key>}} reference in a header) instead.",
      "resolution": "Create the value with `POST /secrets` and write `{{secret:<secret_id>}}` where the literal was (for example `Authorization: Bearer {{secret:sec_...}}`); `meta.paths` names every field to change.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#literal_credential"
    },
    "MCP_TOOL_ERROR": {
      "http_status": 502,
      "description": "An mcp-type tool call reached the MCP server and came back a failure: a JSON-RPC `error` member, or a result the server flagged `isError`. The error `meta` carries the tool name and the server URL. Distinct from `TOOL_HTTP_ERROR`, which is the transport itself refusing the call.",
      "resolution": "The MCP server answered the call with a failure rather than a result — `meta.mcp_tool` names the tool and `meta.mcp_url` the server. The message carries what the server said; fix the arguments or the server-side condition it names, then retry. The call reached the server, so this is not a connectivity or allowlist problem.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#mcp_tool_error"
    },
    "MEMORY_ALREADY_INVALIDATED": {
      "http_status": 409,
      "description": "The memory no longer holds — a retraction or a supersede already invalidated it — so there is nothing to retract. `meta.memory_id` names it; read it by id to see which.",
      "resolution": "The fact is already out of every read. Read the memory by id to see whether a supersede replaced it, and retract the memory that replaced it if that is the one that no longer holds.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#memory_already_invalidated"
    },
    "MEMORY_RULE_VALIDATION_FAILED": {
      "http_status": 400,
      "description": "The memory rule configuration is invalid (e.g. agent_id and tool_id are both set, `on` is not a supported event, the built-in extractor is bound to conversation_messages.generated, an extractor override is combined with a handler, or source_agent_ids is not an array of agent ids).",
      "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#memory_rule_validation_failed"
    },
    "MEMORY_STORE_NOT_FOUND": {
      "http_status": 400,
      "description": "A referenced memory store 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#memory_store_not_found"
    },
    "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_NOT_PRICED": {
      "http_status": 409,
      "description": "The project runs with `require_priced_model` and no price-book row covers a billable token component of the model this generation would run on, so the spend it would incur could not be measured. The check runs before the provider is called: nothing is sent, spent or metered. `meta.unpriced_rows` names each `(provider, model, component)` to price. An agent bound to a model route is held to every target it may fail over to, not just the first. Price the rows, or clear `require_priced_model` on the project to accept unmeasurable spend.",
      "resolution": "Add price book rows for every `(provider, model, component)` in `meta.unpriced_rows` — globally with `PUT /api/v1/usage/prices`, or for one provider instance with `PUT /api/v1/ai-providers/{ai_provider_id}/prices`. To run unpriced models again, set `require_priced_model` to `false` with `PATCH /api/v1/projects/{project_id}`.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#model_not_priced"
    },
    "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"
    },
    "ON_BEHALF_OF_INVALID": {
      "http_status": 400,
      "description": "The `On-Behalf-Of` header is not an unexpired tool assertion this deployment signed (`meta.reason: unverified`), or the request is not made with an API key scoped to the project the assertion names (`meta.reason: project`).",
      "resolution": "Forward the tool assertion of the call being served unchanged, within its lifetime, with the API key of the project it names; or drop the header to write as the key.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#on_behalf_of_invalid"
    },
    "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. 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_orchestration_run_depth`; the error meta names the depth reached, the limit, and which of the two set it.",
      "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_orchestration_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_RUN_NOT_PAUSABLE": {
      "http_status": 409,
      "description": "The orchestration run has already settled, so there is nothing left to pause. Unlike cancel, a pause keeps the run's last checkpoint and the existing resume route re-drives it from there — so it only applies to a run that is queued, running, sleeping or 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_pausable"
    },
    "ORCHESTRATION_RUN_PAUSED": {
      "http_status": 409,
      "description": "An operator pause is in force on the run, so the requested action was refused. An operator pause has no payload to supply, and a pause standing behind a human or approval node must not be lifted by satisfying it — resume the run first, then submit.",
      "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_paused"
    },
    "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": "The model answered and the answer does not satisfy `output_schema` — the violated field is named in the message. Retrying the same request bills a second call to reach the same answer; loosen the schema, or make the instructions name the shape the model keeps missing.",
      "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 and no `output`, 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"
    },
    "PLAYBOOK_AUTHOR_UNAVAILABLE": {
      "http_status": 409,
      "description": "The case's playbook version has no author a node can run as: the user or API key that wrote it no longer exists, or it was written with a trigger or OAuth token, which carry their boundary in the token.",
      "resolution": "Write the playbook's definition again as a user or API key that can start its executors; cases created on that version run as them.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_author_unavailable"
    },
    "PLAYBOOK_CASE_BUSY": {
      "http_status": 409,
      "description": "An upgrade of a case while one of its executions is running: it would settle against nodes the new version may not have. `meta.executions` names each by `id` and `node`.",
      "resolution": "Wait for the executions `meta.executions` names to settle — close a workflow task, or let an orchestration run end — then retry the upgrade.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_case_busy"
    },
    "PLAYBOOK_CASE_NOT_PAUSED": {
      "http_status": 409,
      "description": "The case carries no pause, so there is nothing to resume.",
      "resolution": "Nothing to do: the case is already running.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_case_not_paused"
    },
    "PLAYBOOK_CASE_PAUSED": {
      "http_status": 409,
      "description": "The case is paused, so none of its nodes starts: neither the cascade nor a run of one node. Executions already in flight settle. Resume the case first.",
      "resolution": "Resume the case with `POST /api/v1/playbook-cases/{case_id}/resume`, then retry.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_case_paused"
    },
    "PLAYBOOK_EXECUTION_FAILED": {
      "http_status": 422,
      "description": "A node's executor settled without a value the node can hold as its output: an agent that returned neither text nor an object, a decision or orchestration run that failed, or a task closed without a payload. Recorded on the execution's `error`, never answered by a request.",
      "resolution": "Read the execution's `error` and the run it names in `run_id`, fix the executor, and run the node again.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_execution_failed"
    },
    "PLAYBOOK_EXECUTION_INTERRUPTED": {
      "http_status": 500,
      "description": "Recorded on an execution, never returned by a request: the process running the node's agent, tool or decider stopped before it settled. It is not run again on its own, so a call with side effects never runs twice; run the node again.",
      "resolution": "Run the node again with `POST /api/v1/playbook-cases/{case_id}/nodes/{node}/run` once whatever its executor called is known not to need undoing.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_execution_interrupted"
    },
    "PLAYBOOK_EXECUTOR_FORBIDDEN": {
      "http_status": 403,
      "description": "The playbook version's author may not start an executor the definition names. Refused when the version is written, and again when a node runs if the author's permission was removed since. `meta` names the `node` and the IAM `action`.",
      "resolution": "Grant the author the start action `meta.action` names on the executor, or have someone who holds it write the playbook's definition, which makes them the author of the new version.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_executor_forbidden"
    },
    "PLAYBOOK_EXPORT_FAILED": {
      "http_status": 422,
      "description": "Recorded on an export, never returned by a request: the transformation's tool failed, or answered no file. A tool answers a file with `execute.response_mode: base64`.",
      "resolution": "Read the export's `error`, make the transformation's tool answer a file (`execute.response_mode: base64`), and export the node again.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_export_failed"
    },
    "PLAYBOOK_EXPORT_FORMAT_MISMATCH": {
      "http_status": 422,
      "description": "Recorded on an export, never returned by a request: the transformation's tool answered a file whose content type is not its `format`'s. `meta` carries both.",
      "resolution": "Make the tool answer the content type of the transformation's `format`, or declare the format it produces, and export the node again.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_export_format_mismatch"
    },
    "PLAYBOOK_EXPORT_INTERRUPTED": {
      "http_status": 500,
      "description": "Recorded on an export, never returned by a request: the process rendering it stopped before it settled. It is not rendered again on its own; export the node again.",
      "resolution": "Export the node again with `POST /api/v1/playbook-cases/{case_id}/nodes/{node}/exports`.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_export_interrupted"
    },
    "PLAYBOOK_HAS_CASES": {
      "http_status": 409,
      "description": "The playbook has cases and cannot be deleted; a case is the record of what was filled against it.",
      "resolution": "Delete the playbook’s cases first, or keep the playbook and stop creating cases from it.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_has_cases"
    },
    "PLAYBOOK_INPUT_CONFLICT": {
      "http_status": 409,
      "description": "An input write's `expected_revision` is not the field's current revision: someone wrote it since it was read. `meta.conflicts` lists each field with both revisions; nothing in the batch was written.",
      "resolution": "Re-read the case, decide each conflicting field against its current value, and retry with the revisions `meta.conflicts` names.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_input_conflict"
    },
    "PLAYBOOK_INPUT_INVALID": {
      "http_status": 400,
      "description": "An input write names a node or input the case's playbook version does not declare, an input that reads another node, or a value its input's schema refuses.",
      "resolution": "Read the case's `nodes` for the inputs its version declares and which of them are filled by a person; correct the value to its input's schema.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_input_invalid"
    },
    "PLAYBOOK_NODE_NOT_CONFIRMABLE": {
      "http_status": 409,
      "description": "The node cannot be confirmed: it is not a `confirm` node, or a required input is unset or reads a node that is not `done`. `meta` carries the `state`, `missing` and `blocked_by`.",
      "resolution": "Read the case: confirm only a `confirm` node, after filling what `missing` names and once the nodes `blocked_by` names are `done`.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_node_not_confirmable"
    },
    "PLAYBOOK_NODE_NOT_EXPORTABLE": {
      "http_status": 409,
      "description": "An export of the latest revision of a node that is not `done`: it has no output yet, or its output is `stale`, `running` or `failed`. `meta` carries the `state`. Name a `revision` to export an earlier output.",
      "resolution": "Wait for the node to settle, or run it again if it `failed`; to export an earlier output, name its `revision` from the case's entries.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_node_not_exportable"
    },
    "PLAYBOOK_NODE_NOT_RUNNABLE": {
      "http_status": 409,
      "description": "The node cannot run now: it is a `confirm` node, a required input is unset or reads a node that is not `done`, or it is already `running` or `waiting` on the task or run it started. `meta` carries the `state`, `missing` and `blocked_by`.",
      "resolution": "Read the case: fill what `missing` names, wait for the nodes `blocked_by` names, or confirm a `confirm` node instead of running it. A `running` node settles on its own; a `waiting` one settles when the task or run its `run` names closes or ends.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_node_not_runnable"
    },
    "PLAYBOOK_SELF_WRITE": {
      "http_status": 403,
      "description": "A write into a case from the work it is running: a run-as credential minted for the case, for one of its executions in flight, or for the task or orchestration run one started. An executor's output reaches its case only as the node's output.",
      "resolution": "Return the value as the executor's result instead: it becomes the node's output, which the nodes reading it take as input.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_self_write"
    },
    "PLAYBOOK_VALIDATION_FAILED": {
      "http_status": 400,
      "description": "The playbook definition is invalid: a duplicate node or input, an input reading a missing node or field, an executor on a node that may not have one or missing from one that must, transformations on a non-final node, two transformations of one name on a node, or a cycle. `meta.problems` lists every one.",
      "resolution": "Fix every problem `meta.problems` lists and retry. `POST /api/v1/playbooks/validate` answers the same check without writing.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#playbook_validation_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"
    },
    "PROJECT_NOT_PAUSED": {
      "http_status": 409,
      "description": "The project carries no pause, so there is nothing to resume.",
      "resolution": "Nothing to do: the project is already running. Read `paused_at` on `GET /api/v1/projects/{project_id}` before resuming.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#project_not_paused"
    },
    "PROJECT_PAUSED": {
      "http_status": 409,
      "description": "The project is paused, so nothing new starts in it: no generation, chat completion, tool call, orchestration run, eval run, decision or trigger firing, and no resume of a run or task the pause holds. Reads and configuration writes still work. `meta` carries `project_id`, `paused_at` and `pause_reason`.",
      "resolution": "Resume the project with `POST /api/v1/projects/{project_id}/resume` once whatever paused it is resolved; `meta.pause_reason` says why it was paused. Nothing is queued while paused — retry the request after resuming.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#project_paused"
    },
    "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"
    },
    "PUBLIC_SHARES_DISABLED": {
      "http_status": 403,
      "description": "A share with `grantee: \"*\"` offers the resource to every project, which this deployment does not allow. Name one grantee project, or set `SHARES_ALLOW_PUBLIC=true` on the server.",
      "resolution": "Create the share with a project id as `grantee`, or ask the operator to set `SHARES_ALLOW_PUBLIC=true`.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#public_shares_disabled"
    },
    "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_STORAGE_EXCEEDED": {
      "http_status": 409,
      "description": "An enforced `storage_bytes` quota is exceeded: the project's stored footprint — files, document chunks (text and vector) and memories — is already over the byte limit, and the write would have added to it. No `Retry-After` is sent, and the refusal is a 409 rather than a 429, because a stock is not a rate: no window reset clears it and waiting changes nothing. The error meta carries `current_bytes` (the last daily snapshot plus the request's own delta) beside `limit`, so the caller knows how much to delete. Raised at the corpus write paths — file upload/create, document create, document ingest and re-ingest, memory create.",
      "resolution": "Delete stored content — files, documents, or memories — until the project is back under the cap; `meta.current_bytes` and `meta.limit` say by how much. Waiting does not help: a storage quota caps a stored total, not a rate. Alternatively raise the cap with `PATCH /api/v1/quotas/{quota_id}`, or switch it to `monitor` mode to stop blocking while you measure.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#quota_storage_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"
    },
    "RESERVED_PATH": {
      "http_status": 400,
      "description": "The path is under '/.system/', which is reserved for files and documents the platform writes on your behalf. Choose a path outside it.",
      "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#reserved_path"
    },
    "RESERVED_TAG_KEY": {
      "http_status": 400,
      "description": "The tag key is reserved: keys starting with 'system.' are written by the platform to record which conversation, actor, agent and role a row came from, and cannot be set by a caller.",
      "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#reserved_tag_key"
    },
    "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"
    },
    "SESSION_NOT_FOUND": {
      "http_status": 400,
      "description": "A referenced session 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#session_not_found"
    },
    "SHARE_CAP_EXCEEDED": {
      "http_status": 429,
      "description": "The call reached another project's tool or agent through a share whose `cap` is spent for the current window, so nothing ran and nothing was metered. The response carries a `Retry-After` header, and `meta` names the share, its `calls` and `window`, `resets_at` and `retry_after`.",
      "resolution": "Wait `retry_after` seconds for the share's window to reset, or ask the publisher to raise its cap.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#share_cap_exceeded"
    },
    "SHARE_IN_USE": {
      "http_status": 409,
      "description": "The grantee project still names the shared resource. `meta.references` lists each of its resources that does; revoking the acceptance leaves them degraded.",
      "resolution": "Repoint or remove the resources `meta.references` lists, or pass `force=true` to revoke anyway and let them degrade.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#share_in_use"
    },
    "SHARE_REVOKED": {
      "http_status": 403,
      "description": "The share was revoked by its publisher, or the publisher revoked this project's acceptance of it. A revoked share accepts nothing and cannot be suspended or resumed; a revoked acceptance refuses a new accept from the same project until the publisher deletes it.",
      "resolution": "A revoked share cannot be used again: ask the publisher for a new share. If the publisher revoked only your acceptance, it must delete that acceptance before you can accept again.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#share_revoked"
    },
    "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).",
      "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.",
      "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_MIGRATION_CONFLICT": {
      "http_status": 409,
      "description": "The task cannot change workflow version now: it is closed, has a transition pending approval (`meta.pending_transition`), or has a dispatch in flight whose routing belongs to the version it was dispatched under.",
      "resolution": "Let the dispatch settle or resolve the pending approval, then migrate again; a closed task stays on its version.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#task_migration_conflict"
    },
    "TASK_MIGRATION_STATE_UNMAPPED": {
      "http_status": 400,
      "description": "The task's current state is not declared by the target workflow version and the migration's `state_map` does not map it. `meta.state` names it.",
      "resolution": "Add `state_map: { \"<meta.state>\": \"<a state the target version declares>\" }` to the migration.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#task_migration_state_unmapped"
    },
    "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_NOT_PAUSABLE": {
      "http_status": 409,
      "description": "The task is closed, so there is no automation left to pause. A workflow has no run object, so an operator pause lands on its instance — the task — and suppresses every state dispatch until it is resumed.",
      "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_not_pausable"
    },
    "TASK_NOT_PAUSED": {
      "http_status": 409,
      "description": "The task carries no operator pause, so there is nothing to resume. Resuming is only how a pause is lifted; a task that is merely idle is advanced by firing a transition.",
      "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_not_paused"
    },
    "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`, or a migration's `state_map` target, does not name a declared state of the workflow version. `meta.state` names it.",
      "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_FAILED": {
      "http_status": 500,
      "description": "Recorded on a generation transcript's tool result, never returned by a request: a tool call made during the turn failed with an error that carries no code of its own, and the model received the failure as the call's result. `meta` carries the fields the thrown error held (`name`, an HTTP tool's `status`, `url`, `body`, …).",
      "resolution": "Read `meta` for what the tool raised. The turn went on with the failure as the call result, so fix the tool or its target for the next turn.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#tool_call_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 tool call was settled by a guardrail before it ran — blocked (class D or a class-B tripwire), or routed to human approval (class C) on a path with nowhere to park. The error `meta` carries the `tool_id` and the `outcome` that settled it. Raised on every dispatch that cannot await sign-off: a direct `POST /tools/{tool_id}/call`, a pipeline step, a trigger firing a tool target, and a workflow on_enter `tool` dispatch. Reach the tool through an agent or an orchestration instead when it is approval-gated: both can park on the approval and resume from it.",
      "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 outbound request the deployment makes to a URL a tenant chose — an http- or mcp-type tool's target, a webhook's `url`, an AI provider's `base_url`, a service-account key file's `token_uri` — names a destination that 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`. A webhook delivery refused this way is closed rather than retried, with the reason on the delivery row.",
      "resolution": "The deployment only makes outbound requests to publicly routable addresses, and this destination is not one — `meta.tool_url` names it and `meta.tool_address` the address it resolved to. Point the tool, webhook or provider `base_url` at a public host, or have the operator list this destination in TOOL_EGRESS_ALLOWED_HOSTS. It is not a permissions problem: no policy grants it.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#tool_egress_blocked"
    },
    "TOOL_HAS_DEPENDENTS": {
      "http_status": 409,
      "description": "The tool is the backend of one or more deciders, or another project has accepted a share of it, and it cannot be deleted. `meta.decider_count` counts the deciders, which block even under `force=true`: delete them or point them at another backend first. `meta.accepted_share_count` counts the accepted shares and `meta.ingestion_rule_count` the ingestion rules of the tool's own project converting with it; both block only until `force=true`, which revokes the shares and leaves the rules naming a converter that is gone.",
      "resolution": "Delete the deciders that name the tool, or point them at another backend with `PATCH /api/v1/deciders/{decider_id}`, then delete the tool. Accepted shares (`meta.accepted_share_count`) are revoked by retrying with `force=true`; ingestion rules (`meta.ingestion_rule_count`) are repointed with `PATCH /api/v1/ingestion-rules/{ingestion_rule_id}`, or left without a converter by `force=true`.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#tool_has_dependents"
    },
    "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"
    },
    "TOOL_RESPONSE_TOO_LARGE": {
      "http_status": 502,
      "description": "An http-type tool with `execute.response_mode: base64` answered with a body over the deployment's ceiling (`TOOL_RESPONSE_MAX_BYTES`, the file upload ceiling by default). The body is refused while it streams, so nothing was buffered. The error `meta` carries `tool_url` and `max_bytes`.",
      "resolution": "The target's file is over this deployment's ceiling (`meta.max_bytes`). Fetch a smaller file, or ask the operator to raise `TOOL_RESPONSE_MAX_BYTES`.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#tool_response_too_large"
    },
    "TOOL_UNREACHABLE": {
      "http_status": 502,
      "description": "An http-type tool call never got a response: the target's host did not resolve, refused or dropped the connection, or timed out. The message names the network failure and the error `meta` carries the URL and method. Distinct from `TOOL_HTTP_ERROR`, which is the target answering with a non-2xx status.",
      "resolution": "Check the tool's URL and that its host is reachable from the server; the message names the network failure. A timeout or a dropped connection may succeed on retry.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#tool_unreachable"
    },
    "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_FIRING_ABANDONED": {
      "http_status": 500,
      "description": "An event firing was interrupted without reaching a result, repeatedly, and is not attempted again. `meta.attempts` is how many starts it had. It appears as a firing's recorded `error`, never as a response: by the time it is written there is nobody left to answer.",
      "resolution": "The dispatch did not survive long enough to record an outcome. Check the target named by the trigger for work that ends the process, then re-fire the trigger manually.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#trigger_firing_abandoned"
    },
    "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"
    },
    "UPLOAD_TOO_LARGE": {
      "http_status": 413,
      "description": "The multipart upload exceeds the byte ceiling this deployment accepts (`FILE_UPLOAD_MAX_BYTES`, 25 MB by default). The request is refused while the body is still streaming, so nothing was stored. Send a smaller file, or ask the operator to raise the ceiling.",
      "resolution": "The upload is over this deployment's byte ceiling. Send a smaller file, or ask the operator to raise `FILE_UPLOAD_MAX_BYTES`.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#upload_too_large"
    },
    "VALIDATION_FAILED": {
      "http_status": 400,
      "description": "The request body or query string 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, and for an unknown query parameter lists `unknown_query_parameters` and `accepted_query_parameters`.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#validation_failed"
    },
    "VERSION_CONFLICT": {
      "http_status": 409,
      "description": "The write named a version other than the one the resource currently holds, or a concurrent write took that version first. `meta.current_version` is the version in force, and `meta.expected_version` the one the request named. Nothing was written: re-read the resource, rebase the change on what it now holds, and retry against the version `meta.current_version` names.",
      "resolution": "Re-read the resource, re-apply the change on top of the version `meta.current_version` names, and retry with that version. A retry of the same body against the same stale version conflicts again.",
      "docs_url": "https://soat.ttoss.dev/docs/error-codes#version_conflict"
    },
    "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_MIGRATION_NOT_FOUND": {
      "http_status": 404,
      "description": "The workflow migration does not exist, or belongs to another workflow.",
      "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_migration_not_found"
    },
    "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"
    },
    "WORKFLOW_VERSION_NOT_FOUND": {
      "http_status": 400,
      "description": "The workflow has no version with that number. `meta.version` names it; list the workflow versions to see which 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#workflow_version_not_found"
    }
  }
}
