{
  "openapi": "3.0.3",
  "info": {
    "title": "naturali.ai API",
    "version": "1.0.0",
    "description": "The naturali.ai API. This document is the merge of every per-resource spec under openapi/v1/ — the single wire contract for all v1 endpoints.",
    "contact": {
      "name": "naturali.ai",
      "url": "https://naturali.ai"
    }
  },
  "servers": [
    {
      "url": "{baseUrl}",
      "description": "Host of your naturali.ai deployment; every path carries the /v1 prefix.",
      "variables": {
        "baseUrl": {
          "description": "Base host URL.",
          "default": "https://api.naturali.ai"
        }
      }
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "oauth2": [
        "mcp:access"
      ]
    }
  ],
  "tags": [
    {
      "name": "Activity",
      "description": "Read the autonomous-execution activity feed"
    },
    {
      "name": "Actors",
      "description": "Manage actors associated with projects"
    },
    {
      "name": "Channels",
      "description": "Connect and manage a project's messaging surfaces (WhatsApp, Discord)."
    },
    {
      "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": "Create, scope, rotate and revoke nat_sk_ API keys."
    },
    {
      "name": "Approvals",
      "description": "Manage the human-decision approval queue"
    },
    {
      "name": "Assistant",
      "description": "Link and revoke the channel identities allowed to operate your naturali account through the Naturali Assistant.\n"
    },
    {
      "name": "Audit Log",
      "description": "Query the append-only audit log"
    },
    {
      "name": "Auth",
      "description": "Sign in, refresh and end interactive human sessions."
    },
    {
      "name": "Chains",
      "description": "Inspect continuation chains and how large they have grown"
    },
    {
      "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": "Installs",
      "description": "Install listings into a project, and remove them."
    },
    {
      "name": "Knowledge",
      "description": "Unified search across documents and knowledge sources"
    },
    {
      "name": "Listings",
      "description": "Publish a tool or an agent, and see who installed it."
    },
    {
      "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": "Models",
      "description": "Browse the model catalog. The catalog is the same for every project.\n"
    },
    {
      "name": "Orchestrations",
      "description": "Manage orchestrations and their runs"
    },
    {
      "name": "Projects",
      "description": "Create and manage projects — the per-client / per-environment boundary."
    },
    {
      "name": "Quotas",
      "description": "Manage quotas and rate limits"
    },
    {
      "name": "Secrets",
      "description": "Manage secrets"
    },
    {
      "name": "Sessions",
      "description": "Manage agent sessions"
    },
    {
      "name": "Tasks",
      "description": "Manage tasks and their transitions"
    },
    {
      "name": "Tools",
      "description": "Manage tools"
    },
    {
      "name": "Traces",
      "description": "Inspect execution traces and trace trees"
    },
    {
      "name": "Triggers",
      "description": "Manage triggers and inspect firings"
    },
    {
      "name": "Users",
      "description": "Read and edit the authenticated account."
    },
    {
      "name": "Webhooks",
      "description": "Subscribe an endpoint to a project's events, and audit what was delivered."
    },
    {
      "name": "Workflows",
      "description": "Manage workflow definitions"
    }
  ],
  "paths": {
    "/v1/projects/{project_id}/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": "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"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of activity entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ActivityEntry"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Pass as `cursor` to fetch the next page; `null` when this is the last page"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed cursor"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-mcp-exclude": true,
        "parameters": [
          {
            "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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/actors": {
      "get": {
        "tags": [
          "Actors"
        ],
        "summary": "List actors",
        "description": "Returns all actors in the project named in the path.",
        "operationId": "listActors",
        "parameters": [
          {
            "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"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of actors",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ActorRecord"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Actors"
        ],
        "summary": "Create an actor",
        "description": "Creates a new actor in the project named in the path.",
        "operationId": "createActor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "additionalProperties": false,
                "properties": {
                  "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-naturali-ref": "agents",
                    "type": "string",
                    "description": "Agent to link this actor to. Mutually exclusive with chat_id.",
                    "example": "agent_V1StGXR8Z5jdHi6B"
                  },
                  "chat_id": {
                    "x-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/actors/{actor_id}": {
      "get": {
        "tags": [
          "Actors"
        ],
        "summary": "Get an actor by ID",
        "description": "Returns an actor by its ID",
        "operationId": "getActor",
        "x-naturali-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-naturali-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-naturali-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-naturali-ref": "agents",
                    "type": "string",
                    "nullable": true,
                    "description": "Agent to link this actor to. Mutually exclusive with chat_id."
                  },
                  "chat_id": {
                    "x-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/actors/{actor_id}/tags": {
      "get": {
        "tags": [
          "Actors"
        ],
        "summary": "Get actor tags",
        "description": "Returns all tags attached to the actor",
        "operationId": "getActorTags",
        "x-naturali-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-naturali-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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/addresses": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "List addresses",
        "description": "Every identifier this project has seen. An address appears the first time a message arrives from it, so this is what naturali has observed rather than a roster you maintain — most rows carry no `action` of their own and defer to the route table.\n",
        "operationId": "listAddresses",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of addresses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddressList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/addresses/{identifier}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/Identifier"
        }
      ],
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "Get an address",
        "description": "Reads one identifier's own action, if it has one. An identifier naturali has never seen is a `404`; to set an action without knowing whether it exists, use [`PUT /v1/projects/{project_id}/addresses/{identifier}`](/docs/api/addresses/set-address-action), which upserts.\n",
        "operationId": "getAddress",
        "responses": {
          "200": {
            "description": "The address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Address"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Channels"
        ],
        "summary": "Set or clear this address's own action",
        "description": "Upserts the address and its `action` in one call. `action: null` forgets the exception and defers back to the route table; any other `action` requires the same fields a route or channel default would (`agent_id` for `agent`, `text` for `message`).\n",
        "operationId": "setAddressAction",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddressActionSet"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Action updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Address"
                }
              }
            }
          },
          "201": {
            "description": "Address created with this action.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Address"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Channels"
        ],
        "summary": "Erase an address",
        "description": "Removes the address, its conversations, and its runtime actor and sessions.\n",
        "operationId": "deleteAddress",
        "responses": {
          "204": {
            "description": "Address erased."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/addresses/{identifier}/conversations": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/Identifier"
        }
      ],
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "List an address's conversations",
        "description": "Every conversation this address has had, across every channel it has ever messaged — an address is project-scoped, not channel-scoped.\n",
        "operationId": "listAddressConversations",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of conversations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/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": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of agents",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Agent"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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-naturali-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-naturali-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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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 /v1/projects/{project_id}/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-naturali-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          },
          {
            "name": "wait",
            "in": "query",
            "required": false,
            "x-naturali-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`. An MCP tool call always waits.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAgentGenerationRequest"
              },
              "examples": {
                "basic": {
                  "summary": "Simple generation",
                  "value": {
                    "messages": [
                      {
                        "role": "user",
                        "content": "What is the weather in Tokyo?"
                      }
                    ]
                  }
                },
                "toolOutput": {
                  "summary": "Use a tool output as user message content",
                  "value": {
                    "messages": [
                      {
                        "role": "user",
                        "content": {
                          "type": "tool_output",
                          "tool_id": "tool_audio_to_text",
                          "input": {
                            "url": "https://example.com/audio.mp3"
                          },
                          "output_path": "text"
                        }
                      }
                    ]
                  }
                },
                "streaming": {
                  "summary": "Streaming generation",
                  "value": {
                    "messages": [
                      {
                        "role": "user",
                        "content": "Summarize the latest report."
                      }
                    ],
                    "stream": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Generation result or SSE stream (only when `?wait=true` or `stream: true`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentGenerationResponse"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "SSE stream of delta chunks ending with `data: [DONE]`.\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 /v1/projects/{project_id}/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 /v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-resource": {
          "kind": "agent",
          "from": "agent_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AgentId"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of agent versions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AgentVersion"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Agent not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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": "limit",
            "in": "query",
            "description": "Number of results per page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of AI providers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "provider": {
                            "type": "string",
                            "enum": [
                              "openai",
                              "anthropic",
                              "google",
                              "xai",
                              "groq",
                              "ollama",
                              "azure",
                              "bedrock",
                              "vertex",
                              "gateway",
                              "custom",
                              "naturali"
                            ]
                          },
                          "default_model": {
                            "type": "string"
                          },
                          "secret_id": {
                            "x-naturali-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-naturali-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": {
                  "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",
                      "naturali"
                    ],
                    "description": "LLM provider",
                    "example": "openai"
                  },
                  "default_model": {
                    "type": "string",
                    "description": "Default model to use",
                    "example": "gpt-4"
                  },
                  "secret_id": {
                    "x-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-ref": "projects",
                      "type": "string"
                    },
                    "secret_id": {
                      "x-naturali-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",
                      "naturali"
                    ],
                    "description": "LLM provider",
                    "example": "openai"
                  },
                  "default_model": {
                    "type": "string"
                  },
                  "secret_id": {
                    "x-naturali-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 (nulls) its usage history, preserving those rows. The `409` body's `error.meta` reports the counts, a sample of offending IDs, and a `forcible` flag that is `true` when a `force=true` retry would succeed.\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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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.\n",
        "operationId": "listAiProviderModels",
        "parameters": [
          {
            "name": "ai_provider_id",
            "in": "path",
            "required": true,
            "description": "AI Provider ID",
            "schema": {
              "type": "string",
              "example": "aip_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The models this provider can run",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderModelsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The provider type or authentication mode cannot enumerate models (including Vertex express mode), or the record is missing configuration the listing needs (a Vertex project from either `config.project` or the service-account key file, a Bedrock region, or — for the API-key provider types — a linked secret).\n"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "AI provider not found"
          },
          "502": {
            "description": "The provider rejected the listing request"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/api-keys": {
      "get": {
        "tags": [
          "API Keys"
        ],
        "summary": "List API keys",
        "description": "Lists API keys accessible to the caller. A project-scoped credential sees only keys in its project; an account-scoped credential sees all keys in the account. Raw secrets are never returned.\n",
        "operationId": "listApiKeys",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of API keys.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "API Keys"
        ],
        "summary": "Create an API key",
        "description": "Creates an API key. When `project_id` is set the key is scoped to that project (the default and recommended stance); omit it for an account-scoped key. `capabilities` narrows what the key may do; when omitted the key inherits the creator's capabilities. The raw `key` (nat_sk_…) is returned only in this response.\nA project-scoped key requires membership of that project. The key carries no role of its own — every request it makes resolves its holder's membership again — so it can never reach past what its minter already had. An account-scoped key requires a credential that is not itself confined to one project.\nA connected app cannot create keys at all: a grant is revocable and a key is not, so a key minted under a grant would still work after the app was disconnected. Reading and revoking keys stay available.\n",
        "operationId": "createApiKey",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiKeyCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "API key created. The raw key value is only returned here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ApiKeyForbidden"
          }
        }
      }
    },
    "/v1/api-keys/{api_key_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ApiKeyId"
        }
      ],
      "get": {
        "tags": [
          "API Keys"
        ],
        "summary": "Get an API key",
        "description": "Returns metadata for an API key. The raw secret is never returned after creation.",
        "operationId": "getApiKey",
        "responses": {
          "200": {
            "description": "API key metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyRecord"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ApiKeyForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "API Keys"
        ],
        "summary": "Update an API key",
        "description": "Rename an API key or replace its capability set. The scope (project vs account) is immutable.",
        "operationId": "updateApiKey",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiKeyUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "API key updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyRecord"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ApiKeyForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "API Keys"
        ],
        "summary": "Revoke an API key",
        "description": "Revokes an API key immediately. Subsequent use returns 401.",
        "operationId": "deleteApiKey",
        "responses": {
          "204": {
            "description": "API key revoked."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ApiKeyForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/api-keys/{api_key_id}:rotate": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ApiKeyId"
        }
      ],
      "post": {
        "tags": [
          "API Keys"
        ],
        "summary": "Rotate an API key",
        "description": "Issues a new secret for the same key record (same id, scope and capabilities) and invalidates the previous secret. The new raw `key` is returned only in this response.\nRefused for a connected app, like creation: rotation returns a raw secret for a key the caller need never have held, so it reaches the same place by another door.\n",
        "operationId": "rotateApiKey",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Rotated. The new raw key value is only returned here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyCreated"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ApiKeyForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/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": "status",
            "in": "query",
            "description": "Filter by lifecycle status",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "approved",
                "rejected",
                "expired"
              ]
            }
          },
          {
            "name": "origin",
            "in": "query",
            "description": "Filter by producer origin",
            "schema": {
              "type": "string",
              "enum": [
                "node",
                "tool_call",
                "task_transition"
              ]
            }
          },
          {
            "name": "expires_before",
            "in": "query",
            "description": "Return only items expiring at or before this timestamp",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of approval items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApprovalItem"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid `status` or `origin` filter value"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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": "status",
            "in": "query",
            "description": "Lifecycle status the groups are built from (default `rejected`)",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "approved",
                "rejected",
                "expired"
              ],
              "default": "rejected"
            }
          },
          {
            "name": "min_count",
            "in": "query",
            "description": "Minimum items in a group for it to be returned",
            "schema": {
              "type": "integer",
              "default": 2,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of groups to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of groups to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of recurrence groups",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApprovalRecurrenceGroup"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid `status` filter value"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/assistant/grants": {
      "get": {
        "tags": [
          "Assistant"
        ],
        "summary": "List linked identities",
        "description": "Lists the caller's assistant grants — one per channel identity that may operate their account. Grants belong to the account, so this is the caller's own set regardless of which projects they own.\n",
        "operationId": "listAssistantGrants",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of grants.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssistantGrantList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/assistant/grants/{grant_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/GrantId"
        }
      ],
      "delete": {
        "tags": [
          "Assistant"
        ],
        "summary": "Revoke a linked identity",
        "description": "Revokes the grant, disabling the Assistant for that identity. The grant is resolved on every inbound message, so the next one from that identity is refused before the agent is invoked — revocation is immediate, not eventual. The identity is free to link again afterwards.\n",
        "operationId": "revokeAssistantGrant",
        "responses": {
          "204": {
            "description": "Grant revoked."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/assistant/link": {
      "get": {
        "tags": [
          "Assistant"
        ],
        "summary": "Resolve a pending link",
        "description": "Resolves a link token **without consuming it**, so the confirmation screen can name the identity being linked (\"@user on Discord\") before anyone commits to it. Naming it is what makes a link pasted into the wrong hands fail the human check as well as the server-side binding.\nReading is deliberately separate from redeeming: a single-use nonce must not be burned by a link preview, a URL scanner or a browser prefetch.\n",
        "operationId": "previewAssistantLink",
        "parameters": [
          {
            "$ref": "#/components/parameters/LinkToken"
          }
        ],
        "responses": {
          "200": {
            "description": "The identity this token would link, and what it would be allowed to do.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssistantLinkPreview"
                }
              }
            }
          },
          "400": {
            "description": "The token is unknown, expired, or already redeemed (`invalid_token` / `expired_token`). Getting a fresh one means messaging the Assistant again.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Assistant"
        ],
        "summary": "Redeem a link token",
        "description": "Redeems a link token and creates the grant, binding the channel identity the token carries to the authenticated account. The identity is read from the token server-side — nothing in this request can point the link at a different one.\nThe token is the idempotency key: it is single-use, so a replay of this request fails rather than creating a second grant.\n",
        "operationId": "redeemAssistantLink",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssistantLinkRedeem"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Grant created. The Assistant answers that identity from the next message on.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssistantGrant"
                }
              }
            }
          },
          "400": {
            "description": "The token is missing, unknown, expired, or already redeemed (`invalid_token` / `expired_token`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "The identity is already linked to an account (`identity_already_linked`). One channel identity operates one naturali account: re-pointing it means revoking the existing grant first, which is explicit and auditable rather than silent.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assistant/messages": {
      "get": {
        "tags": [
          "Assistant"
        ],
        "summary": "Read the conversation",
        "description": "The newest 100 messages of your conversation with the Assistant in the app, oldest first. An account that has not messaged it yet has an empty conversation.\n",
        "operationId": "listAssistantMessages",
        "responses": {
          "200": {
            "description": "The conversation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssistantMessageList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ConversationForbidden"
          },
          "502": {
            "$ref": "#/components/responses/TurnUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/AssistantUnavailable"
          }
        }
      },
      "post": {
        "tags": [
          "Assistant"
        ],
        "summary": "Message the Assistant",
        "description": "Runs one turn as your account and answers with the messages it added: yours, then the Assistant's reply. The turn is paid from your credit and refused before it runs when the balance is below zero.\n",
        "operationId": "sendAssistantMessage",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssistantMessageCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The messages this turn added, oldest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssistantMessageList"
                }
              }
            }
          },
          "400": {
            "description": "`text` is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Your credit balance is below zero (`insufficient_credit`). Top up to message the Assistant again.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ConversationForbidden"
          },
          "502": {
            "$ref": "#/components/responses/TurnUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/AssistantUnavailable"
          }
        }
      }
    },
    "/v1/projects/{project_id}/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": "action",
            "in": "query",
            "description": "Exact permission-action string, e.g. `secrets:DeleteSecret`",
            "schema": {
              "type": "string",
              "example": "secrets:DeleteSecret"
            }
          },
          {
            "name": "principal_id",
            "in": "query",
            "description": "Public id of the principal (`user_…` or `key_…`)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "resource_public_id",
            "in": "query",
            "description": "Exact target resource public id, e.g. `sec_…`",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "resource_srn",
            "in": "query",
            "description": "SRN prefix match, e.g. `srn:{project}:secret:`. The log is append-only, so a stored SRN is never rewritten; the filter matches it as stored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Only entries created at or after this timestamp (ISO 8601)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Only entries created at or before this timestamp (ISO 8601)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Number of results per page (1–200, default 25)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 25
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of audit entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AuditEntry"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`from` or `to` is present but not a valid ISO 8601 date"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-mcp-exclude": true,
        "parameters": [
          {
            "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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/auth/code": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Email a sign-in code",
        "description": "Emails a short numeric code to the address. Always responds 200 with the same body whether or not the address has an account, so it never leaks existence. A first-time address gets an account on its first successful verification, so this is both sign-up and log-in. Issuing a code invalidates any previous one for the same address.\n",
        "operationId": "requestSignInCode",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignInCodeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "If the address can receive a code, one has been sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Acknowledgement"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/v1/auth/code/verify": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Redeem a sign-in code",
        "description": "Exchanges an emailed code for a session, creating the account if the address is new. The code is single-use and short-lived. A code is destroyed after too many wrong guesses, since six digits is small enough to guess given unlimited attempts — the client must then request a new one rather than retry.\n",
        "operationId": "verifySignInCode",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignInCodeVerify"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authenticated; a session is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthSession"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "The code is wrong, expired, or already used (`invalid_token` / `expired_token`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Too many incorrect attempts (`too_many_attempts`); the code has been destroyed and a new one must be requested.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/google": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Sign in with Google",
        "description": "Exchanges a Google ID token, issued to this deployment's web client, for a session. The account is the one for the token's address: an address that signed in with an emailed code reaches the same account, and a new address gets one. Google must have verified the address.\n",
        "operationId": "signInWithGoogle",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GoogleSignIn"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authenticated; a session is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthSession"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "The token is malformed, expired, not signed by Google or issued to another client (`invalid_token`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Google has not verified the address (`email_not_verified`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Sign-in with Google is not configured on this deployment (`google_sign_in_not_configured`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/refresh": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Refresh a session",
        "description": "Exchanges a valid refresh token for a new access JWT and a rotated refresh token, taken from the body or from the `refresh_token` cookie set at sign-in — a browser sends an empty body and the cookie carries the credential. Refresh tokens are single-use; presenting a previously-rotated token is treated as reuse and revokes the whole session family (createRefreshRotation reuse detection), except within a few seconds of the rotation, where it is treated as two tabs racing on one cookie and rotated again.\n",
        "operationId": "refreshSession",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefreshRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A new session (access + rotated refresh).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthSession"
                }
              }
            }
          },
          "401": {
            "description": "The refresh token is invalid, expired or was reused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/logout": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Log out",
        "description": "Revokes the current refresh token (and its rotation family) and clears the `refresh_token` cookie. Pass `all: true` to revoke every active session for the user.\n",
        "operationId": "logout",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LogoutRequest"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Session(s) revoked."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/projects/{project_id}/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": "status",
            "in": "query",
            "description": "Filter by chain status",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "concluded",
                "expired",
                "budget_exhausted"
              ]
            }
          },
          {
            "name": "agent_id",
            "in": "query",
            "description": "Filter by the agent whose continuation opened the chain",
            "schema": {
              "type": "string",
              "example": "agent_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of continuation chains",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Chain"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/channel-kinds": {
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "List channel kinds",
        "description": "Every connectable kind, its surfaces, and the predicates its routes can match on (engine-wide ones like `address_known` plus its own, like Discord's `guild_id`).\n",
        "operationId": "listChannelKinds",
        "responses": {
          "200": {
            "description": "Every channel kind.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelKindList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/projects/{project_id}/channels/{channel_id}/routes": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/ChannelId"
        }
      ],
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "List a channel's routes",
        "description": "One channel's routes, newest first, `disabled` ones included — only `active` routes take part in resolution. For every route in the project in one read, use [`GET /v1/projects/{project_id}/channel-routes`](/docs/api/channel-routes/list-project-channel-routes).\n",
        "operationId": "listChannelRoutes",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of routes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelRouteList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Channels"
        ],
        "summary": "Create a route",
        "description": "`action` is required; `agent_id` is required when it is `agent`, `text` when it is `message`; `repeat` is only valid alongside `action: message`. `surface`, when present, must be one this channel's kind serves (`GET /v1/channel-kinds`) — otherwise `400 unknown_surface`. `match` keys must be predicates that kind's registry declares — otherwise `400 unknown_predicate`. Writing a route for a surface whose transport mode is off (e.g. a Discord `guild_thread` route before `mention_threads` is enabled) still succeeds, with `unreachable_surface` in the response `warnings`.\n",
        "operationId": "createChannelRoute",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChannelRouteWrite"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Route created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelRoute"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/projects/{project_id}/channels/{channel_id}/routes/{route_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/ChannelId"
        },
        {
          "$ref": "#/components/parameters/RouteId"
        }
      ],
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "Get a route",
        "description": "A route id is only meaningful inside its own channel: the same id under another `channel_id` is a `404`. A conversation's `route_id` may instead be one of the two sentinels for the address's own action and the channel default — those name no route and are not readable here.\n",
        "operationId": "getChannelRoute",
        "responses": {
          "200": {
            "description": "The route.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelRoute"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Channels"
        ],
        "summary": "Replace a route",
        "description": "Full replace, the same validation as create: an omitted field takes its create default. Where that default would widen a value the route sets, omission is `400 replace_omits_fields`, naming the fields in `details.fields`, and nothing is written: a set `surface` (the default is any surface), a non-empty `match` (every message), a non-empty `config` (it carries the Discord allowlist), and `status` on a `disabled` route (the default re-activates it). Send the value to keep it, or `\"surface\": null`, `\"match\": {}`, `\"config\": null`, `\"status\": \"active\"` to clear it.\n",
        "operationId": "updateChannelRoute",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChannelRouteWrite"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Route replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelRoute"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      },
      "delete": {
        "tags": [
          "Channels"
        ],
        "summary": "Delete a route",
        "description": "Conversations opened under this route keep its id and stay where they are; the next inbound resolves to the next most specific route, or the channel default, and opens a new conversation there. To stop a route from matching while keeping it readable, `PATCH` it to `status: disabled` instead.\n",
        "operationId": "deleteChannelRoute",
        "responses": {
          "204": {
            "description": "Route deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/channel-routes": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "Read the whole route table",
        "description": "Every route across every channel in the project, one read — the table is small by construction (CHANNELS-ROUTING.md §3.6), so this is unpaginated.\n",
        "operationId": "listProjectChannelRoutes",
        "responses": {
          "200": {
            "description": "Every route in the project.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChannelRoute"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/channels": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "List channels",
        "description": "Lists the channels connected in the project.",
        "operationId": "listChannels",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of channels.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Channels"
        ],
        "summary": "Connect a channel",
        "description": "Connect a channel. The required fields depend on `channel`; no credential is ever returned.\n**Discord** (`channel: discord`) — supply `application_id` and `bot_token`, plus the `modes` selecting which Gateway flows to serve (direct messages, and/or @mention-opens-a-thread). The token is checked with Discord first: `400 invalid_bot_token` when Discord rejects it, `400 application_mismatch` when it belongs to another application. Returns `501` when the deployment has no `CHANNEL_TOKEN_KEY` configured.\n**WhatsApp** (default) — one of two credential paths, both filling the same write-only secret:\n* **BYOT** (`credential_source: byot`, default) — supply\n  `phone_number_id` and `access_token` from your own Meta app.\n\n* **Embedded signup** (`credential_source: embedded_signup`) — supply\n  `code` and `waba_id` (and optionally `pin`) from the Meta popup;\n  naturali exchanges the `code` for the token and subscribes its app to\n  the WABA, so the customer never hands over a token. Returns `501` on\n  deployments where Meta App credentials are not configured.\n\n\nThe project's plan limits how many channels it may hold. At the limit this responds `403` with `plan_limit_reached`, whose `details` carry the `plan` and the `limit`, before any credential is acquired. Only `active` channels count, so disabling one frees allowance without deleting it.\n",
        "operationId": "createChannel",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChannelCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Channel created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Channel"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamUnavailable"
          }
        }
      }
    },
    "/v1/projects/{project_id}/channels/{channel_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/ChannelId"
        }
      ],
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "Get a channel",
        "description": "The channel's state, its transport modes and its default action. The access token is never returned — `has_credential` is all a read says about it.\n",
        "operationId": "getChannel",
        "responses": {
          "200": {
            "description": "Channel details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Channel"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Channels"
        ],
        "summary": "Update a channel",
        "description": "Rotate the credential, update the kind's config (`waba_id` / `credential_source` for WhatsApp, `modes` for Discord), or flip the naturali-side status. At least one field is required. Rotating a WhatsApp token stores a new write-only secret, repoints the send tool and deletes the old secret; rotating a Discord bot token reseals it and the gateway worker reconnects with it.\n",
        "operationId": "updateChannel",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChannelUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Channel updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Channel"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Channels"
        ],
        "summary": "Delete a channel",
        "description": "Deletes the channel and whatever it provisioned — the WhatsApp send tool and write-only credential secret, or the Discord channel's sealed bot token (dropped with the row, closing its gateway connection).\n",
        "operationId": "deleteChannel",
        "responses": {
          "204": {
            "description": "Channel deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamUnavailable"
          }
        }
      }
    },
    "/v1/projects/{project_id}/channels/{channel_id}/conversations": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/ChannelId"
        }
      ],
      "post": {
        "tags": [
          "Channels"
        ],
        "summary": "Open a conversation",
        "description": "The outbound-first path: open a conversation for an `identifier` ahead of any inbound message, which falls out of making the identifier the unit rather than the message. Resolves the same three-layer action an inbound would; a `409` when that does not land on an agent — there is nothing to open for a `message`/`silence` outcome.\n",
        "operationId": "openChannelConversation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "identifier"
                ],
                "properties": {
                  "identifier": {
                    "type": "string",
                    "description": "The bare, channel-native identifier (a phone number, a Discord user id).",
                    "example": "425678901234567890"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Conversation opened (or already existing).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Conversation"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamUnavailable"
          }
        }
      },
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "List the channel's conversations",
        "description": "A cursor page of the channel's conversations, newest first. A conversation is one address's dialogue on the channel — the continuity anchor that resumes the same runtime session instead of starting fresh per message — so this is the read path for who has talked to the channel and which actor/session their dialogue resolved to.\nConversations are created by the inbound path (the WhatsApp webhook, the Discord gateway worker) when a real message arrives; there is no way to create one directly.\n",
        "operationId": "listChannelConversations",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "identifier",
            "in": "query",
            "required": false,
            "description": "Filter to one address by its bare, channel-native identifier (a phone number on WhatsApp; a Discord user id, or `thread:<id>` for a guild thread) — the same value the channel itself reports, not the prefixed form the `identifier` field above returns. At most one row matches.\n",
            "schema": {
              "type": "string",
              "example": "425678901234567890"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of conversations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/channels/{channel_id}/conversations/{conversation_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/ChannelId"
        },
        {
          "$ref": "#/components/parameters/ConversationId"
        }
      ],
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "Get a conversation",
        "description": "Where the conversation points: the address it belongs to, the route (or sentinel) whose action opened it, and the runtime session it maps 1:1 to. The messages are not here — read them with [`GET /v1/projects/{project_id}/channels/{channel_id}/conversations/{conversation_id}/messages`](/docs/api/channels/list-channel-conversation-messages).\n",
        "operationId": "getChannelConversation",
        "responses": {
          "200": {
            "description": "The conversation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Conversation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Channels"
        ],
        "summary": "Delete a conversation",
        "description": "Removes the conversation and the runtime session it maps to. The address, its actor and its other conversations are left alone; the address's next message opens a fresh conversation. To erase the address itself, use [`DELETE /v1/projects/{project_id}/addresses/{identifier}`](/docs/api/addresses/delete-address).\n",
        "operationId": "deleteChannelConversation",
        "responses": {
          "204": {
            "description": "Conversation deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/channels/{channel_id}/conversations/{conversation_id}/messages": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/ChannelId"
        },
        {
          "$ref": "#/components/parameters/ConversationId"
        }
      ],
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "Read a conversation's transcript",
        "description": "The conversation's messages, oldest first. naturali stores no message bodies — the dialogue lives in the runtime session the conversation maps to, so this reads through to the runtime. Pagination is `limit`/`offset` rather than an opaque cursor because the upstream is offset-based over a stable `position` ordering.\n",
        "operationId": "listChannelConversationMessages",
        "parameters": [
          {
            "$ref": "#/components/parameters/MessagesLimit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of messages.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationMessageList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamUnavailable"
          }
        }
      }
    },
    "/v1/projects/{project_id}/conversations": {
      "get": {
        "tags": [
          "Conversations"
        ],
        "summary": "List conversations",
        "description": "Returns all conversations in the project named in the path.",
        "operationId": "listConversations",
        "parameters": [
          {
            "name": "actor_id",
            "in": "query",
            "required": false,
            "description": "Filter by actor ID",
            "schema": {
              "type": "string",
              "example": "actor_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "$ref": "#/components/parameters/TagsQuery"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of conversations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConversationRecord"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Conversations"
        ],
        "summary": "Create a conversation",
        "description": "Creates a new conversation in the project named in the path.",
        "operationId": "createConversation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/conversations/{conversation_id}": {
      "get": {
        "tags": [
          "Conversations"
        ],
        "summary": "Get a conversation by ID",
        "description": "Returns a conversation by its ID",
        "operationId": "getConversation",
        "x-naturali-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-naturali-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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-resource": {
          "kind": "conversation",
          "from": "conversation_id"
        },
        "parameters": [
          {
            "name": "conversation_id",
            "in": "path",
            "required": true,
            "description": "Conversation ID",
            "schema": {
              "type": "string",
              "example": "conv_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of messages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConversationMessageRecord"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Conversations"
        ],
        "summary": "Add a message to a conversation",
        "description": "Creates a document from the message text and attaches it to the conversation at the given position. If position is omitted, it is appended at the end.",
        "operationId": "addConversationMessage",
        "x-naturali-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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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 /v1/projects/{project_id}/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-naturali-resource": {
          "kind": "conversation",
          "from": "conversation_id"
        },
        "parameters": [
          {
            "name": "conversation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "wait",
            "in": "query",
            "required": false,
            "x-naturali-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. An MCP tool call 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-naturali-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-Naturali-Context-<key>` headers on every `http` and `mcp` tool call in this generation. The header name is the deployment's configured context prefix (`X-Naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/conversations/{conversation_id}/tags": {
      "get": {
        "tags": [
          "Conversations"
        ],
        "summary": "Get conversation tags",
        "description": "Returns all tags attached to the conversation",
        "operationId": "getConversationTags",
        "x-naturali-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-naturali-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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/deciders": {
      "get": {
        "tags": [
          "Deciders"
        ],
        "summary": "List deciders",
        "description": "Returns the deciders defined in a project, newest first.",
        "operationId": "listDeciders",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "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 `state` 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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/deciders/{decider_id}": {
      "get": {
        "tags": [
          "Deciders"
        ],
        "summary": "Get a decider",
        "description": "Returns a decider with its current question set and version.",
        "operationId": "getDecider",
        "x-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-resource": {
          "kind": "decider",
          "from": "decider_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/DeciderId"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/deciders/{decider_id}/decisions": {
      "post": {
        "tags": [
          "Deciders"
        ],
        "summary": "Request a decision",
        "description": "Evaluates the decider against `state` and records the decision. The questions come from the decider, never from the request, so a call site can only supply what is judged.\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 `{ state, questions }` and must answer `{ answers: { <question id>: { choice | score | value, probabilities? } } }`; 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 /v1/projects/{project_id}/decisions/{decision_id}` or subscribe to `decisions.completed` and `decisions.failed`. With `wait: true` it is `201` with the decision settled.",
        "operationId": "createDecision",
        "x-naturali-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 state, 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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/decisions": {
      "get": {
        "tags": [
          "Deciders"
        ],
        "summary": "List decisions",
        "description": "Returns the decisions in a project, newest first.",
        "operationId": "listDecisions",
        "parameters": [
          {
            "name": "decider_id",
            "in": "query",
            "description": "Only decisions requested from this decider",
            "schema": {
              "type": "string",
              "example": "dcd_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Only decisions in this status",
            "schema": {
              "type": "string",
              "enum": [
                "queued",
                "running",
                "completed",
                "failed"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/documents": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "List documents",
        "description": "Returns all documents in the project named in the path.",
        "operationId": "listDocuments",
        "parameters": [
          {
            "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"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of documents",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DocumentRecord"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Documents"
        ],
        "summary": "Create a document",
        "description": "Creates a new text document and generates an embedding vector for semantic search, in the project named in the path.",
        "operationId": "createDocument",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "additionalProperties": false,
                "properties": {
                  "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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-ref": "files",
                    "type": "string",
                    "description": "ID of the uploaded file. Must be one of application/pdf, text/plain, text/markdown.",
                    "example": "file_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
                  }
                }
              }
            }
          }
        },
        "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 /v1/projects/{project_id}/documents/{document_id}` until `status` is `ready` or `failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestedDocumentRecord"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request, file not found, or unsupported content type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "The file already backs a Document (a file can only be ingested once — use `POST /documents/{document_id}/ingest` to re-process the existing document, or upload a new copy of the file to ingest it separately), 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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-mcp-exclude": true,
        "parameters": [
          {
            "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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The document's relations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DocumentRelation"
                      }
                    }
                  }
                }
              }
            }
          },
          "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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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 /v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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
                  }
                }
              }
            }
          }
        },
        "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 /v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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 served but not metered.\n",
        "operationId": "createEmbeddings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "input": {
                    "type": "string",
                    "description": "Single text to embed.",
                    "example": "The quick brown fox jumps over the lazy dog."
                  },
                  "inputs": {
                    "type": "array",
                    "description": "Batch of texts to embed. 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",
            "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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/datasets": {
      "get": {
        "tags": [
          "Evaluations"
        ],
        "summary": "List datasets",
        "description": "Returns the datasets defined in a project",
        "operationId": "listDatasets",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of datasets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Dataset"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "post": {
        "tags": [
          "Evaluations"
        ],
        "summary": "Create a dataset",
        "description": "Creates a project-scoped dataset — a named collection of test cases an eval runs an agent against. Names are unique per project.\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": {
                  "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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/datasets/{dataset_id}": {
      "get": {
        "tags": [
          "Evaluations"
        ],
        "summary": "Get a dataset",
        "description": "Returns a specific dataset",
        "operationId": "getDataset",
        "x-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/datasets/{dataset_id}/items": {
      "get": {
        "tags": [
          "Evaluations"
        ],
        "summary": "List dataset items",
        "description": "Returns the test cases in a dataset, oldest first",
        "operationId": "listDatasetItems",
        "x-naturali-resource": {
          "kind": "dataset",
          "from": "dataset_id"
        },
        "parameters": [
          {
            "name": "dataset_id",
            "in": "path",
            "required": true,
            "description": "Dataset ID",
            "schema": {
              "type": "string",
              "example": "dset_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of dataset items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DatasetItem"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Dataset not found"
          }
        }
      },
      "post": {
        "tags": [
          "Evaluations"
        ],
        "summary": "Add a dataset item",
        "description": "Adds one test case. `input` is replayed verbatim as the generation's messages, so it must be a non-empty array of `{ role, content }`.",
        "operationId": "createDatasetItem",
        "x-naturali-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."
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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)"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/evals": {
      "get": {
        "tags": [
          "Evaluations"
        ],
        "summary": "List evals",
        "description": "Returns the evals defined in a project",
        "operationId": "listEvals",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of evals",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Eval"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "post": {
        "tags": [
          "Evaluations"
        ],
        "summary": "Create an eval",
        "description": "Binds an agent under test to a dataset and a list of scorers. The agent and the dataset must belong to the same project as the eval; a cross-project reference is rejected with 400.\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": {
                  "name": {
                    "type": "string",
                    "description": "Unique name within the project",
                    "example": "billing-regression-suite"
                  },
                  "agent_id": {
                    "x-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/evals/{eval_id}": {
      "get": {
        "tags": [
          "Evaluations"
        ],
        "summary": "Get an eval",
        "description": "Returns a specific eval",
        "operationId": "getEval",
        "x-naturali-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-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/evals/{eval_id}/runs": {
      "get": {
        "tags": [
          "Evaluations"
        ],
        "summary": "List eval runs",
        "description": "Returns an eval's runs, newest first",
        "operationId": "listEvalRuns",
        "x-naturali-resource": {
          "kind": "eval",
          "from": "eval_id"
        },
        "parameters": [
          {
            "name": "eval_id",
            "in": "path",
            "required": true,
            "description": "Eval ID",
            "schema": {
              "type": "string",
              "example": "eval_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of eval runs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EvalRun"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Eval not found"
          }
        }
      },
      "post": {
        "tags": [
          "Evaluations"
        ],
        "summary": "Start an eval run",
        "description": "Runs the eval against its dataset, creating one real agent generation per item and scoring the outputs.\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. 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-naturali-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-Naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of eval results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EvalResult"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Eval or run not found"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/evals/{eval_id}/runs/{eval_run_id}/cancel": {
      "post": {
        "tags": [
          "Evaluations"
        ],
        "summary": "Cancel an eval run",
        "description": "Cancels a queued or running run: its outstanding item tasks are dropped so it stops consuming provider budget, and the run settles as `canceled`.\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-naturali-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 canceled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvalRun"
                }
              }
            }
          },
          "400": {
            "description": "The run has already finished"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Eval or run not found"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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": "status",
            "in": "query",
            "description": "Filter by triage status",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "acknowledged",
                "resolved"
              ]
            }
          },
          {
            "name": "severity",
            "in": "query",
            "description": "Filter by severity",
            "schema": {
              "type": "string",
              "enum": [
                "info",
                "warning",
                "critical"
              ]
            }
          },
          {
            "name": "kind",
            "in": "query",
            "description": "Filter by how the exception was filed",
            "schema": {
              "type": "string",
              "enum": [
                "run_failed",
                "guardrail_tripwire",
                "approval_expired",
                "quota_unpriced",
                "event_trigger_loop",
                "chain_limit",
                "manual"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of exception items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ExceptionItem"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/files": {
      "get": {
        "tags": [
          "Files"
        ],
        "summary": "List all files",
        "description": "Returns a list of all stored files",
        "operationId": "listFiles",
        "parameters": [
          {
            "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"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of files returned successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FileRecord"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Files"
        ],
        "summary": "Create a file",
        "description": "Creates a new file record in the system",
        "operationId": "createFile",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/files/{file_id}/download": {
      "get": {
        "tags": [
          "Files"
        ],
        "summary": "Download a file",
        "description": "Streams the file content to the client",
        "operationId": "downloadFile",
        "x-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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": {
                  "formation_id": {
                    "x-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/formations": {
      "get": {
        "tags": [
          "Formations"
        ],
        "summary": "List formations",
        "description": "Returns all formation stacks for a project",
        "operationId": "listFormations",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of formations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Formation"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "Formations"
        ],
        "summary": "Create a new formation",
        "description": "Validates the template, creates the formation record, then provisions all declared resources in dependency order.\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 `naturali` 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": {
                  "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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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 `naturali` 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 /v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of operations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FormationOperation"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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",
            "schema": {
              "type": "string",
              "enum": [
                "in_progress",
                "requires_action",
                "completed",
                "failed"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of generations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Generation"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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 /v1/projects/{project_id}/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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of guardrails",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Guardrail"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/guardrails/{guardrail_id}": {
      "get": {
        "tags": [
          "Guardrails"
        ],
        "summary": "Get a guardrail",
        "description": "Returns a single guardrail by ID.",
        "operationId": "getGuardrail",
        "x-naturali-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-naturali-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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-resource": {
          "kind": "guardrail",
          "from": "guardrail_id"
        },
        "parameters": [
          {
            "name": "guardrail_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of guardrail versions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/GuardrailVersion"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Guardrail not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/ingestion-rules": {
      "get": {
        "tags": [
          "Ingestion Rules"
        ],
        "summary": "List ingestion rules",
        "description": "Returns the ingestion rules for a project",
        "operationId": "listIngestionRules",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Number of results per page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of ingestion rules",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/IngestionRule"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "post": {
        "tags": [
          "Ingestion Rules"
        ],
        "summary": "Create an ingestion rule",
        "description": "Creates a rule mapping a content_type glob to a converter. Exactly one of tool_id or agent_id must be set.",
        "operationId": "createIngestionRule",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content_type_glob"
                ],
                "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 — a MIME glob needs one on each side of the slash at most.",
                    "maxLength": 255,
                    "example": "image/*"
                  },
                  "tool_id": {
                    "x-naturali-ref": "tools",
                    "type": "string",
                    "description": "Converter tool id (mutually exclusive with agent_id)",
                    "example": "tool_V1StGXR8Z5jdHi6B"
                  },
                  "agent_id": {
                    "x-naturali-ref": "agents",
                    "type": "string",
                    "description": "Converter agent id (mutually exclusive with tool_id)",
                    "example": "agent_V1StGXR8Z5jdHi6B"
                  },
                  "action": {
                    "type": "string",
                    "description": "Operation id, required for 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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/ingestion-rules/{ingestion_rule_id}": {
      "get": {
        "tags": [
          "Ingestion Rules"
        ],
        "summary": "Get an ingestion rule",
        "description": "Returns a specific ingestion rule",
        "operationId": "getIngestionRule",
        "x-naturali-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-naturali-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-naturali-ref": "tools",
                    "type": "string",
                    "nullable": true
                  },
                  "agent_id": {
                    "x-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/installs": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Installs"
        ],
        "summary": "List installs",
        "description": "The listings the project installed, newest first.",
        "operationId": "listInstalls",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of installs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InstallList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Installs"
        ],
        "summary": "Install a listing",
        "description": "Install a listing by its id. A listed entry, a draft and a listing in review all install; a suspended one answers `409 listing_suspended`. Installing again answers the existing install.\nAfter the publisher removed this project's access, installing answers `403 install_blocked`.\n",
        "operationId": "createInstall",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InstallCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The project had already installed the listing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Install"
                }
              }
            }
          },
          "201": {
            "description": "Listing installed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Install"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InstallConflict"
          }
        }
      }
    },
    "/v1/projects/{project_id}/installs/{install_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/InstallId"
        }
      ],
      "get": {
        "tags": [
          "Installs"
        ],
        "summary": "Get an install",
        "description": "One of the project's installs.",
        "operationId": "getInstall",
        "responses": {
          "200": {
            "description": "The install.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Install"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Installs"
        ],
        "summary": "Uninstall a listing",
        "description": "Remove the install. While the project's agents, ingestion rules or other resources still name the installed resource, this answers `409 install_in_use` with them in `details.references`; `force=true` uninstalls anyway and those resources stop reaching it.\nA `blocked` install, and the install managed conversion uses, answer `403`.\n",
        "operationId": "deleteInstall",
        "parameters": [
          {
            "name": "force",
            "in": "query",
            "required": false,
            "description": "Uninstall even while resources still use it.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Uninstalled."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InstallConflict"
          }
        }
      }
    },
    "/v1/projects/{project_id}/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": {
                  "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-naturali-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-naturali-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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/listings": {
      "get": {
        "tags": [
          "Listings"
        ],
        "summary": "Browse the marketplace",
        "description": "Every listed entry, newest first. `interface` is what an installing project sees of the resource, read live from the publisher.\n",
        "operationId": "listPublicListings",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of public listings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicListingList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/listings/{listing_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ListingId"
        }
      ],
      "get": {
        "tags": [
          "Listings"
        ],
        "summary": "Get a public listing",
        "description": "One listed entry. A listing that is not listed answers `404`.",
        "operationId": "getPublicListing",
        "responses": {
          "200": {
            "description": "The public listing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicListing"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/listings": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Listings"
        ],
        "summary": "List a project's listings",
        "description": "The listings the project publishes, newest first.",
        "operationId": "listListings",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of listings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListingList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Listings"
        ],
        "summary": "Create a listing",
        "description": "Publish one of the project's tools or agents as a `draft`. Any project member may publish.\nAn agent on a naturali model whose model has no price answers `503 model_not_priced`. A resource that does not exist in the project answers `400 resource_not_found`, and one that already has a listing `409 listing_exists`.\n",
        "operationId": "createListing",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListingCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Listing created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listing"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/ListingConflict"
          },
          "503": {
            "$ref": "#/components/responses/ListingModelNotPriced"
          }
        }
      }
    },
    "/v1/projects/{project_id}/listings/{listing_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/ListingId"
        }
      ],
      "get": {
        "tags": [
          "Listings"
        ],
        "summary": "Get a listing",
        "description": "One of the project's listings.",
        "operationId": "getListing",
        "responses": {
          "200": {
            "description": "The listing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listing"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Listings"
        ],
        "summary": "Update a listing",
        "description": "Change the title or description, or pause and resume the listing.\n`state: suspended` stops the listing for every installing project at once; their installs are kept. `state: listed` resumes a listing you suspended, provided naturali had listed it; nobody reinstalls. A listing naturali suspended cannot be resumed here, and an invalid move answers `409 invalid_listing_state`.\n",
        "operationId": "updateListing",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListingUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Listing updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listing"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/ListingConflict"
          },
          "503": {
            "$ref": "#/components/responses/ListingModelNotPriced"
          }
        }
      }
    },
    "/v1/projects/{project_id}/listings/{listing_id}:submit": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/ListingId"
        }
      ],
      "post": {
        "tags": [
          "Listings"
        ],
        "summary": "Submit a listing for review",
        "description": "Move a `draft` listing to `in_review`. naturali lists it in the marketplace or sends it back to `draft`. Any other state answers `409 invalid_listing_state`.\n",
        "operationId": "submitListing",
        "responses": {
          "200": {
            "description": "Listing submitted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listing"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/ListingConflict"
          },
          "503": {
            "$ref": "#/components/responses/ListingModelNotPriced"
          }
        }
      }
    },
    "/v1/projects/{project_id}/listings/{listing_id}/installs": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/ListingId"
        }
      ],
      "get": {
        "tags": [
          "Listings"
        ],
        "summary": "List a listing's installs",
        "description": "Each project that installed the listing, with its usage this billing cycle as last sampled. Never the content of a call.\n",
        "operationId": "listListingInstalls",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of installs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListingInstallList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/listings/{listing_id}/installs/{install_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/ListingId"
        },
        {
          "$ref": "#/components/parameters/InstallId"
        }
      ],
      "delete": {
        "tags": [
          "Listings"
        ],
        "summary": "Remove a project's install",
        "description": "Cut one installing project off. An `active` or `suspended` install becomes `blocked`, and that project cannot install the listing again. Deleting a `blocked` install removes it, which lets that project install again.\n",
        "operationId": "deleteListingInstall",
        "responses": {
          "204": {
            "description": "Install blocked or removed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/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"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "include_invalidated",
            "in": "query",
            "required": false,
            "description": "Include invalidated 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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/memories/{memory_id}": {
      "get": {
        "tags": [
          "Memories"
        ],
        "summary": "Get a memory",
        "description": "Returns a single memory by ID",
        "operationId": "getMemory",
        "x-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/memories/{memory_id}/tags": {
      "get": {
        "tags": [
          "Memories"
        ],
        "summary": "Get memory tags",
        "description": "Returns all tags attached to the memory",
        "operationId": "getMemoryTags",
        "x-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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": "limit",
            "in": "query",
            "description": "Number of results per page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "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-naturali-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-naturali-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-naturali-ref": "agents",
                      "type": "string"
                    },
                    "example": [
                      "agent_V1StGXR8Z5jdHi6B"
                    ]
                  },
                  "agent_id": {
                    "x-naturali-ref": "agents",
                    "type": "string",
                    "nullable": true,
                    "description": "Handler agent (mutually exclusive with tool_id)"
                  },
                  "tool_id": {
                    "x-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/memory-rules/{memory_rule_id}": {
      "get": {
        "tags": [
          "Memory Rules"
        ],
        "summary": "Get a memory rule",
        "description": "Returns a specific memory rule",
        "operationId": "getMemoryRule",
        "x-naturali-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-naturali-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-naturali-ref": "agents",
                      "type": "string"
                    }
                  },
                  "agent_id": {
                    "x-naturali-ref": "agents",
                    "type": "string",
                    "nullable": true
                  },
                  "tool_id": {
                    "x-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/memory-stores": {
      "get": {
        "tags": [
          "MemoryStores"
        ],
        "summary": "List memory stores",
        "description": "Returns a list of memory store configurations for a project",
        "operationId": "listMemoryStores",
        "parameters": [
          {
            "$ref": "#/components/parameters/TagsQuery"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "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": {
                  "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 /v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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-naturali-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 /v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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": "since",
            "in": "query",
            "required": false,
            "description": "Only assertions recorded at or after this instant.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "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 `since` that is not a timestamp"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Memory store not found"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-resource": {
          "kind": "memory_store",
          "from": "memory_store_id"
        },
        "x-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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": "resource_type",
            "in": "query",
            "required": false,
            "description": "Return only the declarations governing this resource.",
            "schema": {
              "type": "string",
              "enum": [
                "document"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "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": {
                  "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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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": {
                  "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": {
                      "type": "string",
                      "nullable": true,
                      "description": "What the metadata violates, field by field.",
                      "example": "/quarter must be equal to one of the allowed values"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/metadata-schemas/{metadata_schema_id}": {
      "get": {
        "tags": [
          "Metadata Schemas"
        ],
        "summary": "Get a metadata schema",
        "description": "Returns one declaration by id.",
        "operationId": "getMetadataSchema",
        "x-naturali-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-naturali-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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/model-routes": {
      "get": {
        "tags": [
          "Model Routes"
        ],
        "summary": "List model routes",
        "description": "Returns the model routes defined in a project",
        "operationId": "listModelRoutes",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of model routes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ModelRoute"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "post": {
        "tags": [
          "Model Routes"
        ],
        "summary": "Create a model route",
        "description": "Creates a project-scoped model route: a named, ordered list of provider+model targets tried in array order. Every target must reference an AI provider in the same project (400 otherwise), and the total attempt budget — the sum of `1 + max_retries` over all targets — may not exceed 10 (400 naming the computed total). A duplicate `name` in the project is rejected with 409.",
        "operationId": "createModelRoute",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "targets"
                ],
                "additionalProperties": false,
                "properties": {
                  "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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/model-routes/{route_id}": {
      "get": {
        "tags": [
          "Model Routes"
        ],
        "summary": "Get a model route",
        "description": "Returns a specific model route",
        "operationId": "getModelRoute",
        "x-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/models": {
      "get": {
        "tags": [
          "Models"
        ],
        "summary": "List models",
        "description": "Lists the models naturali offers, sorted by name. Filter by vendor, input/output modality or lifecycle status.\nEvery model listed here is available to any project with a `provider: \"naturali\"` [AI provider](/docs/api/ai-providers/create-ai-provider) — there is no second class to filter for. Models naturali cannot serve (unpriced, or producing something other than text) are not in this catalog; to generate on one of those, register your own credentials as an [AI provider](/docs/api/ai-providers/create-ai-provider) and ask that provider what it serves with [`GET /v1/projects/{project_id}/ai-providers/{ai_provider_id}/models`](/docs/api/ai-providers/list-ai-provider-models).\n",
        "operationId": "listModels",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "vendor",
            "in": "query",
            "required": false,
            "description": "Filter by model maker (e.g. anthropic, amazon, meta).",
            "schema": {
              "type": "string",
              "example": "amazon"
            }
          },
          {
            "name": "modality",
            "in": "query",
            "required": false,
            "description": "Filter to models whose input or output modalities include this value (e.g. text, image, embedding, speech).\n",
            "schema": {
              "type": "string",
              "example": "text"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by lifecycle status.",
            "schema": {
              "type": "string",
              "enum": [
                "available",
                "deprecated"
              ],
              "example": "available"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of models.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/models/{model}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ModelName"
        }
      ],
      "get": {
        "tags": [
          "Models"
        ],
        "summary": "Get a model",
        "description": "`{model}` is naturali's own name for the model, the same value [`GET /v1/models`](/docs/api/models/list-models) returns — a vendor's invocation string is a `404`. A `deprecated` model still reads here, so a project already generating on one can see what happened to it.\n",
        "operationId": "getModel",
        "responses": {
          "200": {
            "description": "Model details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Model"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/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": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of orchestrations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Orchestration"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/orchestrations/validate": {
      "post": {
        "tags": [
          "Orchestrations"
        ],
        "summary": "Validate an orchestration graph",
        "description": "Statically validates an orchestration graph without persisting anything. Checks that every node has its required field, node ids are unique, edges reference existing nodes, the graph is acyclic (unless it contains a loop node), and every `input_mapping` `{\"var\": \"...\"}` reference resolves to a state key written by an upstream node or seeded by `input_schema`. Returns blocking `errors` and non-blocking `warnings` (e.g. a state key only written on a conditional branch). The same `errors` checks are enforced on create and update, which fail with `400` when any error is present.\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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/orchestrations/queue/stats": {
      "get": {
        "tags": [
          "Orchestrations"
        ],
        "summary": "Get orchestration queue stats",
        "description": "Returns a point-in-time snapshot of the orchestration run queue: how many tasks are waiting to be claimed (`queue_depth`), how many are currently claimed with a valid lease (`claimed_tasks`), the age of the oldest waiting task, recent claim-latency percentiles over a rolling in-process window, and a per-project breakdown. Intended for admin/operator policies; guarded by `orchestrations:GetQueueStats`.\nEvery figure is scoped to what the caller may see. A project-scoped caller gets `per_project` for their own projects only, `queue_depth` and `claimed_tasks` summed over those same 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": []
          },
          {
            "oauth2": [
              "mcp:access"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Queue stats snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueueStats"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/orchestrations/{orchestration_id}": {
      "get": {
        "tags": [
          "Orchestrations"
        ],
        "summary": "Get an orchestration",
        "description": "Returns the orchestration with nodes and edges.",
        "operationId": "getOrchestration",
        "x-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-resource": {
          "kind": "orchestration",
          "from": "orchestration_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/orchestration_id"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of orchestration versions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrchestrationVersion"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Orchestration not found"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/orchestration-runs": {
      "post": {
        "tags": [
          "Orchestrations"
        ],
        "summary": "Start an orchestration run",
        "description": "Creates a new run for the orchestration named by orchestration_id. By default the run executes durably in the background: the response returns immediately with status \"queued\" (a worker then claims it and moves it to \"running\") and progress is observed via get-orchestration-run or run lifecycle webhook events (orchestration_runs.started/awaiting_input/succeeded/failed). Delay and poll waits park the run as \"sleeping\" and are woken by a background scheduler, surviving restarts. Pass wait=true to block until the run reaches a terminal or awaiting_input state.",
        "operationId": "startOrchestrationRun",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/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": "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",
                  "succeeded",
                  "failed",
                  "cancelled",
                  "expired"
                ]
              }
            },
            "style": "form",
            "explode": true,
            "example": [
              "queued",
              "running"
            ]
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of runs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrchestrationRun"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "List projects",
        "description": "Lists the projects the caller is a member of. A project-scoped API key lists only its own project.\n",
        "operationId": "listProjects",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of projects.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Create a project",
        "description": "Creates a project (one per client or per environment).\n\nYour plan limits how many projects you may own. At the limit this responds `403` with `plan_limit_reached`, whose `details` carry the `plan` and the `limit`. An archived project still counts — archiving keeps every resource, so it frees nothing; deleting a project does.\n\nA new project starts at the content-retention window your plan sets (`trace_content_retention_days`); `PATCH` it to anything shorter.\n",
        "operationId": "createProject",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Project created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "The `idempotency_key` is already claimed by a different request (`idempotency_key_reused`), or by one still in flight (`idempotency_request_in_progress`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/projects/{project_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "Get a project",
        "description": "Returns one project you are a member of, and your `role` in it. An id you are not a member of — including one that does not exist — responds `404`, not `403`: the API never confirms that an id exists elsewhere.\n`403` is reserved for the cases where there is nothing to hide: a project you *are* in, addressed with a credential scoped to a different one, or an action your role does not carry. There the message is what makes the failure fixable.\n",
        "operationId": "getProject",
        "responses": {
          "200": {
            "description": "Project details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Projects"
        ],
        "summary": "Update a project",
        "description": "Rename or archive a project, and/or change its content-retention settings (`trace_content_retention_days`, `trace_content_mode`), its execution ceilings (`max_concurrent_runs`, `max_chain_generations`, `max_orchestration_run_depth`), its priced-model gate (`require_priced_model`), its project-scope guardrails (`guardrail_ids`) and managed conversion (`managed_conversion`). Archiving is reversible; resources are retained.\nRequires the `admin` role in the project (an `owner` has it too). These are the terms every member works under, which is why setting them sits above the role that works under them.\nThe ceilings and the priced-model gate are uncapped by plan: each one only ever narrows what the project may spend, so setting one takes on a restriction rather than claiming an entitlement. On the three ceilings `null` clears the project's own bound and omission leaves it alone — they are different instructions.\n`guardrail_ids` is the floor under every tool call by every agent in the project, including tools added later. The list is replaced wholesale, so send the ids you want to keep; `[]` detaches every one. An id naming no guardrail in the project responds `400` with `guardrail_not_found`, whose `details.missing` lists the ids. It is uncapped by plan, like the ceilings: a guardrail can only tighten what runs.\nThe two retention controls answer different questions. The window bounds how long content *stays* — a daily sweep purges anything past it, leaving auditable skeletons behind. `trace_content_mode: none` means content is never *written*, which is the stronger guarantee: it cannot be missed by a sweep or survive in a backup.\n\nYour plan sets the longest window you may keep content for. A wider one — `null` included, which keeps content indefinitely — responds `403` with `plan_limit_reached`, whose `details` carry the `plan` and the `limit` in days. Anything shorter is always allowed. Moving to a plan with a shorter window takes effect at the end of the billing cycle, and content already stored is then purged by age like everything else.\n",
        "operationId": "updateProject",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Project updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "400": {
            "description": "A malformed field, or `guardrail_not_found` for a `guardrail_ids` entry naming no guardrail in the project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Projects"
        ],
        "summary": "Delete a project",
        "description": "Permanently deletes the project and its backing runtime project. Fails with 409 if the runtime project still has dependent resources — remove them first, or pass `force=true` to delete the project and all its dependents (agents, providers, tools, sessions, generations, traces). Forcing is destructive and irreversible.\nRequires the `owner` role — an `admin` runs the project day to day, but destroying it is the billing owner's call.\n",
        "operationId": "deleteProject",
        "parameters": [
          {
            "$ref": "#/components/parameters/Force"
          }
        ],
        "responses": {
          "204": {
            "description": "Project deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The project still has dependent resources on the runtime (retry with `force=true`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/projects/{project_id}/pause": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Pause a project",
        "description": "Stops everything the project runs, with one call. From the moment it answers, nothing new starts: generations, tool calls, orchestration runs, eval runs and trigger fires in the project respond `409` with `PROJECT_PAUSED`, and so does resuming a single run or task the pause holds. A channel keeps receiving messages and answers each with a neutral \"can't reply right now\" line.\nWhat is already in motion is parked, not discarded: live orchestration runs pause at their next checkpoint, open tasks stop dispatching, schedule triggers stop firing and queued eval items wait. A generation already running finishes. Reads and configuration changes keep working.\nIdempotent: pausing a paused project answers it unchanged and keeps the first `reason`. Emits `project.paused`. Requires the `admin` role.\n",
        "operationId": "pauseProject",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectPause"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The paused project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/resume": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Resume a project",
        "description": "Lifts the project's pause and hands back exactly what it held: the runs and tasks it parked resume, 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 paused on its own before the project was paused stays paused. Emits `project.resumed`. Requires the `admin` role.\n",
        "operationId": "resumeProject",
        "responses": {
          "200": {
            "description": "The resumed project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The project is not paused (`project_not_paused`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/projects/{project_id}/members": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "List project members",
        "description": "Lists who may act in the project, and with what role. Readable by every member, whatever their role: who else is in the project is not a privileged fact, and hiding it makes \"why can that person see my agents?\" unanswerable.\nSomebody invited who has never signed in is listed too, with status `pending` — the invitation is already a grant, so hiding it until they arrive would hide access that exists.\n",
        "operationId": "listProjectMembers",
        "responses": {
          "200": {
            "description": "The project's members, oldest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectMemberList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Add a project member",
        "description": "Adds a colleague to the project by email address. Free and uncapped on every plan: a member adds no run, no channel, no trigger and no byte, so every cost they cause is already billed through the project's owner. Members are counted nowhere.\n\n**The address does not need an account.** Inviting one that has none creates it unverified and records the membership beside it, so the invitee is already a member with status `pending`. Their first sign-in code — which they request themselves, from the link in the invitation — is what makes them `active`. There is no separate acceptance step and no invitation to expire: the address is the credential, and the invitation reached it.\n\nAn `owner` may grant `admin` or `member`; an `admin` may grant `member` only, so administration cannot hand out its own authority. `role: owner` is refused — that is the billing owner, and transferring it is a different act.\n\nThe invitation email is a courtesy, not the grant: if it cannot be sent the membership still stands, and the response still says `pending`. Requires an account-wide credential — a project-scoped key cannot grant access that would outlive its own revocation.\n",
        "operationId": "addProjectMember",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectMemberCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The member was added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectMember"
                }
              }
            }
          },
          "400": {
            "description": "Missing `email`, or a `role` outside `admin`/`member`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller is a `member`, an `admin` granting `admin`, or a project-scoped credential.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "That address is already a member of this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "This account has sent too many invitations today.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/projects/{project_id}/members/{member_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "name": "member_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The membership ID (`pmem_` prefix), not the user ID.",
          "example": "pmem_V1StGXR8Z5jdHi6B"
        }
      ],
      "patch": {
        "tags": [
          "Projects"
        ],
        "summary": "Change a member's role",
        "description": "Moves a member between `admin` and `member`. The project `owner` only: promoting somebody to `admin` hands them every write in the project, and an `admin` who could do that could erase the difference between the two roles for themselves.\n\nThe owner's own row is refused in either direction — it names who pays, so changing it is a transfer rather than a role change.\n",
        "operationId": "updateProjectMember",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectMemberUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The member's new role.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectMember"
                }
              }
            }
          },
          "400": {
            "description": "A `role` outside `admin`/`member`, or the owner's own row.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Projects"
        ],
        "summary": "Remove a project member",
        "description": "Removes a member, or leaves the project yourself.\n\n**Anyone may remove themselves**, whatever their role: a colleague who no longer wants access should not have to ask for it, and leaving takes nothing from anybody else. Removing *somebody else* needs authority — an `owner` may remove anyone, an `admin` may remove a `member` and not another `admin`.\n\nThe owner's own row is refused: a project with no owner is unreachable, and the row names who pays.\n",
        "operationId": "removeProjectMember",
        "responses": {
          "204": {
            "description": "The member was removed."
          },
          "400": {
            "description": "The row is the project owner's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/usage": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "Get per-project usage",
        "description": "The per-project meter — the re-billing view (A11/C12/P3). Aggregates the project's usage over an optional [from, to] window, bucketed by a single dimension. Costs are the billing-grade cost_usd the runtime freezes at write time; null means nothing in the bucket was priced (never that it was free). Only managed providers are priced (on the runtime), so cost reflects managed usage; BYOK usage carries no LLM cost.\n\nEvery bucket also carries components — the amounts actually measured. The token counts describe LLM usage alone, so that is where a storage, api_request or compute_execution bucket reports its real quantity instead of zeroed token fields.\n\nThe rollup can also be narrowed to one session or one end user before it is bucketed. That is the question no dimension answers: group_by splits the project's whole spend, so \"what did this conversation cost, per day\" and \"what has this end user spent across every session\" are reachable only by narrowing. The narrowings intersect, apply to the totals as well as the buckets, and are echoed back complete — null for each one not applied — because a rollup of zeros is otherwise indistinguishable from a project that spent nothing.\n",
        "operationId": "getProjectUsage",
        "parameters": [
          {
            "name": "group_by",
            "in": "query",
            "required": false,
            "description": "Dimension to bucket by. `day` buckets on the event's UTC calendar day; the others on the matching id. `model` buckets on the model *and* the provider that served it, so one model name served by two providers is two groups (see `ai_provider_id`). `ai_provider` buckets on the provider the spend was billed against — a routed generation's serving target, or the agent's pinned provider — which is the total a per-provider reconciliation wants, without summing the model dimension by hand. `run` buckets on the orchestration run an event belongs to; work that runs no orchestration collapses into the single null bucket, so the bucket count there is not a count of runs. `session` and `actor` bucket on the conversation and the end user behind the spend — what a customer re-billing their own users charges each of them for; traffic with no end user behind it collapses into the single null bucket on both. `source` buckets on what the spend was incurred for (`eval`, `eval_judge`), which is how verification spend is told apart from the traffic serving real users; ordinary traffic carries no source and is the null bucket.\n",
            "schema": {
              "type": "string",
              "enum": [
                "model",
                "ai_provider",
                "agent",
                "run",
                "day",
                "meter_type",
                "actor",
                "session",
                "source"
              ],
              "default": "model"
            }
          },
          {
            "$ref": "#/components/parameters/UsageMeterType"
          },
          {
            "$ref": "#/components/parameters/UsageSessionId"
          },
          {
            "$ref": "#/components/parameters/UsageActorId"
          },
          {
            "$ref": "#/components/parameters/UsageAgentId"
          },
          {
            "$ref": "#/components/parameters/UsageAiProviderId"
          },
          {
            "$ref": "#/components/parameters/UsageOrchestrationRunId"
          },
          {
            "$ref": "#/components/parameters/UsageOrchestrationId"
          },
          {
            "$ref": "#/components/parameters/UsageGenerationId"
          },
          {
            "$ref": "#/components/parameters/UsageTraceId"
          },
          {
            "$ref": "#/components/parameters/UsageSource"
          },
          {
            "$ref": "#/components/parameters/UsageTriggerId"
          },
          {
            "$ref": "#/components/parameters/UsageActionId"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Optional extras to compute. The only value is distinct, which adds the distinct object — one count per kind of entity the window touched. Opt-in because each counter costs the window another sort, so a caller who does not ask keeps the cheap response whatever the event table grows to. Any other value is a 400.\n",
            "schema": {
              "type": "string",
              "enum": [
                "distinct"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Inclusive lower bound (ISO-8601) on event time. Omit for no lower bound.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Inclusive upper bound (ISO-8601) on event time. Omit for no upper bound.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of groups entries to return. Does not affect the top-level totals or groups.total, which describe the whole window. A value above 100 is refused rather than clamped, so a caller paging by their own stride never silently skips a bucket.\n",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of groups entries to skip.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Usage for the project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectUsage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/usage/events": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "List project usage events",
        "description": "The rows a rollup summed — one per metered occurrence, most recent first. Takes the same narrowings [the meter](/docs/api/projects/get-project-usage) does, so a query that produced a bucket answers here unchanged and reads back what went into it. This is the audit and reconciliation view: what was measured, when, against which agent, session, actor and provider, and what each component was priced at.\n\nAn id naming nothing in this project 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": "listProjectUsageEvents",
        "parameters": [
          {
            "$ref": "#/components/parameters/UsageMeterType"
          },
          {
            "$ref": "#/components/parameters/UsageSessionId"
          },
          {
            "$ref": "#/components/parameters/UsageActorId"
          },
          {
            "$ref": "#/components/parameters/UsageAgentId"
          },
          {
            "$ref": "#/components/parameters/UsageAiProviderId"
          },
          {
            "$ref": "#/components/parameters/UsageOrchestrationRunId"
          },
          {
            "$ref": "#/components/parameters/UsageOrchestrationId"
          },
          {
            "$ref": "#/components/parameters/UsageGenerationId"
          },
          {
            "$ref": "#/components/parameters/UsageTraceId"
          },
          {
            "$ref": "#/components/parameters/UsageSource"
          },
          {
            "$ref": "#/components/parameters/UsageTriggerId"
          },
          {
            "$ref": "#/components/parameters/UsageActionId"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of events to return. A value above 100 is refused rather than clamped, so a caller paging by their own stride never silently skips a row.\n",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of events to skip.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of usage events.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectUsageEventPage"
                }
              }
            }
          },
          "400": {
            "description": "A narrowing was given more than once, or a bound is out of range.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/usage/receipt": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "Get a generation or orchestration-run billing receipt",
        "description": "What one occurrence was billed, line by line: per-event line items with their measured components and unit prices, a per-meter-type split, and the totals. Pass generation_id for one generation, or orchestration_run_id for a receipt summed across every event the run metered. Exactly one of the two is required.\n\nThis is the itemisation behind a single number — where [the meter](/docs/api/projects/get-project-usage) says a window cost $12.40, a receipt says which components of which call made up one occurrence of it. On an orchestration-run receipt every line carries node_id, so grouping by it gives the per-node cost the total hides; a retried node contributes one line per attempt, which is the intended reading for spend.\n",
        "operationId": "getProjectUsageReceipt",
        "parameters": [
          {
            "name": "generation_id",
            "in": "query",
            "required": false,
            "description": "Generation public ID. Mutually exclusive with orchestration_run_id.\n",
            "schema": {
              "type": "string",
              "example": "gen_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "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",
              "example": "orun_V1StGXR8Z5jdHi6B"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectUsageReceipt"
                }
              }
            }
          },
          "400": {
            "description": "Neither selector was given, or both were.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No usage was recorded for that id in this project — which is also the answer for an id belonging to another project.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/projects/{project_id}/usage/thresholds": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "List usage thresholds",
        "description": "The spend alerts this project has set. A threshold fires the usage.threshold_crossed event when the project's windowed usage crosses it, so one is only useful alongside a webhook subscribed to that event.\n",
        "operationId": "listProjectUsageThresholds",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of thresholds.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/UsageThreshold"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Set a usage threshold",
        "description": "Creates an alert on this project's windowed usage. Crossing it fires usage.threshold_crossed once per window — a calendar_month threshold fires at most once in a month, and a rolling_24h one re-arms when the windowed total falls back below 90% of the threshold, so a total hovering at the line does not alert repeatedly.\n\nRequires the admin role: an alerting rule belongs to the account that pays, not to one member.\n",
        "operationId": "createProjectUsageThreshold",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UsageThresholdCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The threshold was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageThreshold"
                }
              }
            }
          },
          "400": {
            "description": "An unknown metric or window, or a threshold that is not above zero.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller is a `member`; an alerting rule needs `admin`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/usage/thresholds/{threshold_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "name": "threshold_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "example": "uth_V1StGXR8Z5jdHi6B"
        }
      ],
      "delete": {
        "tags": [
          "Projects"
        ],
        "summary": "Delete a usage threshold",
        "description": "Stops the alert. Requires the admin role.",
        "operationId": "deleteProjectUsageThreshold",
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller is a `member`; an alerting rule needs `admin`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/quotas": {
      "get": {
        "tags": [
          "Quotas"
        ],
        "summary": "List quotas",
        "description": "Returns the quotas defined in a project",
        "operationId": "listQuotas",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of quotas",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Quota"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "500": {
            "description": "Internal server error"
          }
        }
      },
      "post": {
        "tags": [
          "Quotas"
        ],
        "summary": "Create a quota",
        "description": "Creates a project-scoped quota. `requests` is valid for `scope: project`/`api_key`; `tokens` and `cost_usd` are valid for `scope: project`/`agent`/`actor`; `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": {
                  "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 quota.exceeded webhook and writes a quotas:MonitorBreach audit entry, but the request is let through.",
                    "example": "enforce"
                  },
                  "on_unpriced": {
                    "type": "string",
                    "enum": [
                      "block",
                      "allow"
                    ],
                    "default": "block",
                    "description": "Only for metric cost_usd (400 on any other metric). What an enforce quota does when the current window is a pricing blackout — several metered llm_tokens events, none of them priced, so the aggregate is 0 however much was actually spent. Platform meters such as compute_execution are read for the aggregate but never for this verdict. block (the default) refuses new generations with 409 QUOTA_UNENFORCEABLE until pricing is configured; allow accepts the unmeasurable spend explicitly. Either way a quota_unpriced exception is filed. monitor-mode quotas never block regardless. 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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/quotas/{quota_id}": {
      "get": {
        "tags": [
          "Quotas"
        ],
        "summary": "Get a quota",
        "description": "Returns a specific quota, including current window usage",
        "operationId": "getQuota",
        "x-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/secrets": {
      "get": {
        "tags": [
          "Secrets"
        ],
        "summary": "List secrets",
        "description": "Returns a list of secrets for a project",
        "operationId": "listSecrets",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Number of results per page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of secrets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "has_value": {
                            "type": "boolean",
                            "description": "Whether an encrypted value is stored for this secret"
                          },
                          "project_id": {
                            "x-naturali-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-naturali-agent-exclude": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "value"
                ],
                "additionalProperties": false,
                "properties": {
                  "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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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.",
                    "meta": {
                      "session_id": "sess_abc123"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Sessions"
        ],
        "summary": "List sessions",
        "description": "Returns sessions the caller can access, optionally filtered by agent, actor and status.",
        "operationId": "listSessions",
        "parameters": [
          {
            "name": "agent_id",
            "in": "query",
            "required": false,
            "description": "Filter by agent public ID",
            "schema": {
              "type": "string",
              "example": "agent_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "actor_id",
            "in": "query",
            "required": false,
            "description": "Filter by actor public ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by session status (open, closed, or expired)",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "closed",
                "expired"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/TagsQuery"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of sessions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SessionRecord"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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 `GET /v1/projects/{project_id}/usage` with `session_id` / `actor_id` instead.",
        "operationId": "getSession",
        "x-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/sessions/{session_id}/messages": {
      "post": {
        "tags": [
          "Sessions"
        ],
        "summary": "Add a user message",
        "description": "Saves a user message to the session. When autoGenerate is enabled on the session and no generation is currently in progress, generation is triggered automatically and the response mirrors GenerateSessionResponse. Otherwise returns the saved user message.\n",
        "operationId": "addSessionMessage",
        "x-naturali-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddSessionMessageRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Duplicate request — original message returned (idempotency_key matched)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddSessionMessageSaved"
                }
              }
            }
          },
          "201": {
            "description": "User message saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddSessionMessageResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          },
          {
            "name": "wait",
            "in": "query",
            "required": false,
            "x-naturali-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. An MCP tool call 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/GenerateSessionResponse"
                }
              }
            }
          },
          "202": {
            "description": "Generation accepted and running in the background (default, when `wait` is omitted or `false`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "accepted"
                      ]
                    },
                    "session_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Generation already in progress",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "Session has expired due to inactivity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "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 /v1/projects/{project_id}/generations/{generation_id}.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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.\n",
        "operationId": "submitSessionToolOutputs",
        "x-naturali-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/SendSessionMessageResponse"
                }
              }
            }
          },
          "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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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."
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-resource": {
          "kind": "session",
          "from": "session_id"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of forks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SessionRecord"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/sessions/{session_id}/tags": {
      "get": {
        "tags": [
          "Sessions"
        ],
        "summary": "Get session tags",
        "description": "Returns the session's tags object.",
        "operationId": "getSessionTags",
        "x-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/tasks": {
      "get": {
        "description": "Lists tasks (the board query). Filter by workflow, state, status, automation status, or assignee — `GET /tasks?workflow_id=...&state=...` is one board column.",
        "tags": [
          "Tasks"
        ],
        "summary": "List tasks",
        "operationId": "listTasks",
        "parameters": [
          {
            "name": "workflow_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "closed"
              ]
            }
          },
          {
            "name": "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"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A list of tasks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Task"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "description": "Creates a task bound to a workflow. By default the task is placed in the workflow's initial state; passing `state` places it directly in that named state instead — an alternate entry point for starting a task mid-flow (e.g. \"a new recorte for an existing theme by id\"), rather than re-submitting from the initial state and hoping a guard or similarity gate recognizes it. Entering the resulting state, initial or named, behaves identically: that state's `on_enter` automation fires and its `stalled_after` clock arms.",
        "tags": [
          "Tasks"
        ],
        "summary": "Create a task",
        "operationId": "createTask",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTaskRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Task created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — invalid payload (`TASK_PAYLOAD_INVALID`), or `state` does not name a declared state of the workflow (`TASK_STATE_NOT_FOUND`)"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Workflow not found"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/tasks/{task_id}/transitions": {
      "post": {
        "description": "Fires a named transition on a task. The transition must exist in the workflow and be valid from the task's current state; its guard must pass. This is the single path every state change routes through. A transition declaring `requires_approval` does not move the task — it parks a pending ApprovalItem and returns the task with `pending_transition` set; the move applies only when the approval is approved.",
        "tags": [
          "Tasks"
        ],
        "summary": "Transition a task",
        "operationId": "transitionTask",
        "parameters": [
          {
            "name": "task_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TransitionTaskRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The task after the transition",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "400": {
            "description": "The transition does not exist, is not valid, or its guard rejected the move"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Task not found"
          },
          "409": {
            "description": "A concurrent transition made this one invalid, or the task is closed"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/tasks/{task_id}/history": {
      "get": {
        "description": "Returns the append-only transition history of a task.",
        "tags": [
          "Tasks"
        ],
        "summary": "Get task history",
        "operationId": "getTaskHistory",
        "parameters": [
          {
            "name": "task_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The task's transition history, oldest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/TaskTransition"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Task not found"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
                        }
                      }
                    }
                  }
                },
                "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": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of tools",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Tool"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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-naturali-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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/tools/{tool_id}/call": {
      "post": {
        "tags": [
          "Tools"
        ],
        "summary": "Call a tool",
        "description": "Directly invokes a tool and returns its output. Supported for `http`, `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 `mcp` tools the `action` field is required and identifies which tool name to invoke. For `http` tools `action` is ignored. When an `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-naturali-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": {
                "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 — an 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`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/traces": {
      "get": {
        "tags": [
          "Traces"
        ],
        "summary": "List traces",
        "description": "Returns a paginated list of execution traces for the project.",
        "operationId": "listTraces",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of traces",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Trace"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/traces/{trace_id}": {
      "get": {
        "tags": [
          "Traces"
        ],
        "summary": "Get a trace",
        "description": "Returns a single trace by ID.",
        "operationId": "getTrace",
        "x-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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.\n",
        "operationId": "getTraceTree",
        "x-naturali-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)"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated list of related resources to embed on each node. Supported value: `generations` — attaches all generations that belong to each trace node (including sub-agent generations linked via `initiator_generation_id`).\n",
            "schema": {
              "type": "string",
              "example": "generations"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Trace tree rooted at the resolved root trace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TraceTreeNode"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Trace not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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-naturali-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"
                }
              }
            }
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/triggers": {
      "get": {
        "description": "Lists triggers. Filter by project, starter type, or target type.",
        "tags": [
          "Triggers"
        ],
        "summary": "List triggers",
        "operationId": "listTriggers",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "manual",
                "webhook",
                "schedule",
                "event"
              ]
            }
          },
          {
            "name": "target_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "orchestration",
                "agent",
                "tool",
                "eval"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A list of triggers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Trigger"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "description": "Creates a new trigger for a project",
        "tags": [
          "Triggers"
        ],
        "summary": "Create a trigger",
        "operationId": "createTrigger",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTriggerRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Trigger created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TriggerWithSecret"
                }
              }
            }
          },
          "400": {
            "description": "Bad request"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/triggers/{trigger_id}/secret": {
      "get": {
        "description": "Retrieves the signing secret for a webhook trigger",
        "tags": [
          "Triggers"
        ],
        "summary": "Get trigger secret",
        "operationId": "getTriggerSecret",
        "x-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/triggers/{trigger_id}/rotate-secret": {
      "post": {
        "description": "Rotates the signing secret for a webhook trigger",
        "tags": [
          "Triggers"
        ],
        "summary": "Rotate trigger secret",
        "operationId": "rotateTriggerSecret",
        "x-naturali-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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/trigger-firings": {
      "get": {
        "description": "Lists firings for a trigger (trigger_id is required).",
        "tags": [
          "Triggers"
        ],
        "summary": "List trigger firings",
        "operationId": "listTriggerFirings",
        "parameters": [
          {
            "name": "trigger_id",
            "in": "query",
            "required": true,
            "description": "Trigger to list firings for (trg_...)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A list of firings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TriggerFiringListResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Trigger not found"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/users/me": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get the current user",
        "description": "Returns the account the presented credential resolves to.",
        "operationId": "getCurrentUser",
        "responses": {
          "200": {
            "description": "The authenticated user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "patch": {
        "tags": [
          "Users"
        ],
        "summary": "Update the current user",
        "description": "Edits the account's display name and whether it receives usage alerts. At least one field is required, so a request that misspelled a field is rejected rather than answered with a silent 200. Send `name: null` to clear the name.\n\nWith `usage_alerts` on, the account is emailed once each time its model credit, its runs this month or its indexed storage reaches 75%, 90% and 100%. Credit is measured as the share spent this month of what the month had available, so a top-up lowers it. Usage falling back below a level re-arms that level. Figures are read every 15 minutes, so an alert can arrive up to that long after the crossing.\n",
        "operationId": "updateCurrentUser",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/users/me/billing": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get the current account's billing standing",
        "description": "The plan the account is on, how long its projects may keep content, how much indexed storage it is holding, and the credit it has left — the figures the platform already enforces against, readable by the account they are enforced against.\n\nA managed-model generation is refused with `402 insufficient_credit` while `credit_balance_usd` is negative, and a feature or a resource count outside the plan is refused with `403`, as is a retention window wider than `retention_days` or an ingest past `storage_limit_gb`. Every refusal names what is missing; this is where the numbers behind them are read.\n\nEvery figure here is a local read, which is what keeps this route cheap enough to poll — unlike `GET /v1/users/me/usage`, which asks the meter once per project.\n\nAnswers for the caller's own account only, and always the account the credential resolves to — an API key answers for the user that minted it. The plan gating a *project* is that project's billing owner's, so a member of someone else's project reads their own rung here, not that project's.\n",
        "operationId": "getCurrentUserBilling",
        "responses": {
          "200": {
            "description": "The account's plan and credit standing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserBilling"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/users/me/usage": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get the current account's runs this billing cycle",
        "description": "How many runs the account has made this billing cycle, and how many its plan includes. A **run is one agent generation or one tool call** — the unit every rung is priced in. Every tool call counts, including those an agent makes inside a generation; a `client` tool counts nothing.\n\nCounted across the projects this account is the billing owner of, over the current UTC calendar month, from the meter itself. Archived projects are included: the runs they already made were made.\n\n**On the `free` plan the allowance is enforced.** An account that has used it is refused `403 plan_limit_reached` with `resource: \"runs\"` on everything that starts a generation and on a direct tool call — on its own provider credential as much as on a managed model, because a run is a run — until the billing month turns or it upgrades. On `pro` and `business` nothing is refused: runs past the allowance are billed at the overage rate those plans are sold with. A contract plan is not counted.\n\nThe other thing that stops a managed-model generation is a negative credit balance, which is `GET /v1/users/me/billing`, and it never applies to your own credential.\n\n**This route counts live; the refusal reads a figure counted every few minutes.** So the two can differ by a burst: this is the more current number, and a refusal quotes the one it actually enforced.\n\nIt is also more expensive than the balance read: it asks the meter once per project. Poll it on the order of minutes, not seconds.\n",
        "operationId": "getCurrentUserUsage",
        "responses": {
          "200": {
            "description": "The account's runs for the cycle.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserUsage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/users/me/stops": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "List what a billing ceiling stopped",
        "description": "Schedule triggers, eval runs, orchestration runs and workflow tasks the platform stopped on this account's projects because the credit balance went below zero or a Free plan's runs ran out, and that have not been restored or dismissed yet. Newest first.\n\n`blocked_by` names the ceiling still closed: while it is set, nothing can be restored. Top up for `debt`; upgrade or wait for the month to turn for `run_allowance`. A `marketplace_fee` stop, for work naming a priced installed listing, is restored only while the paid balance is positive. A project-confined key sees its own project's stops only.\n",
        "operationId": "listCurrentUserStops",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum items per page — an integer from 1 to 100 (default 20).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of stops still listed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpendStopPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/users/me/stops/{stop_id}/restore": {
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Restore a stopped item",
        "description": "Turns one stopped item back on — re-enables the schedule trigger, or resumes the orchestration run or workflow task — and stops listing it. An item that is no longer stopped (resumed already, finished or deleted) is marked restored too.\n\nRefused with `409 stop_ceiling_closed` while a ceiling is still closed, naming it in `details.blocked_by`, and with `409 stop_not_restorable` for a cancelled eval run, which can only be dismissed.\n",
        "operationId": "restoreCurrentUserStop",
        "parameters": [
          {
            "$ref": "#/components/parameters/stop_id"
          }
        ],
        "responses": {
          "200": {
            "description": "The restored stop.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpendStop"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/StopNotFound"
          },
          "409": {
            "$ref": "#/components/responses/StopNotRestorable"
          },
          "502": {
            "$ref": "#/components/responses/StopRestoreUnavailable"
          }
        }
      }
    },
    "/v1/users/me/stops/{stop_id}": {
      "delete": {
        "tags": [
          "Users"
        ],
        "summary": "Dismiss a stopped item",
        "description": "Stops listing an item without turning it back on. Allowed while a ceiling is still closed.\n",
        "operationId": "dismissCurrentUserStop",
        "parameters": [
          {
            "$ref": "#/components/parameters/stop_id"
          }
        ],
        "responses": {
          "204": {
            "description": "Dismissed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/StopNotFound"
          }
        }
      }
    },
    "/v1/users/me/statements": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "List the account's monthly statements",
        "description": "What the account owes for each closed billing month, newest first: the subscription fee, prorated by the share of the month each plan was held, and runs past the allowance at the plan's overage rate, less what was paid at an upgrade. Overage is measured against the highest plan held that month.\n\nA statement is written within an hour of the month closing, once, and never changes; `payment_status` follows its charge on the saved card. It is not a tax invoice. Months on the Free plan alone, contract plans and storage are not stated.\n",
        "operationId": "listCurrentUserStatements",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum items per page — an integer from 1 to 100 (default 20).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of statements.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatementPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/users/me/statements/{statement_id}": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get a monthly statement",
        "description": "One of this account's monthly statements, with its lines and total. Another account's statement answers `404`.\n",
        "operationId": "getCurrentUserStatement",
        "parameters": [
          {
            "name": "statement_id",
            "in": "path",
            "required": true,
            "description": "The statement's ID (stm_ prefix).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The statement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Statement"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such statement on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/users/me/top-ups": {
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Top up credit",
        "description": "Buys model credit for the caller's account. Once the card is charged the full amount is added to `credit_balance_usd` as purchased credit, which never expires. The card fee is not deducted. `amount_usd` is what is credited; a card issued in Brazil is charged its equivalent in Brazilian reais at the Banco Central's latest closing PTAX selling rate, and a checkout shows reais to a payer in Brazil.\n\nWith `saved_card: true` the saved card is charged at once and the answer is `status: paid`. When the card needs authentication or is declined, nothing is charged and the answer falls back to a checkout.\n\nA checkout (`status: checkout`) is a hosted page: send the caller to `checkout_url`. Creating it charges nothing, and one left unpaid lapses at `expires_at`. The card paid with there becomes the account's saved card, replacing any other. After paying, the browser returns to the console's billing page.\n",
        "operationId": "createCurrentUserTopUp",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TopUpCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The paid top-up, or the checkout to send the caller to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TopUp"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "`saved_card` with no saved card (`payment_method_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "502": {
            "description": "The payment provider could not be reached (`payment_provider_error`). Nothing was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Card payments are not available on this deployment (`payments_not_configured`), or the exchange rate for a card charged in reais could not be read (`exchange_rate_unavailable`). Nothing was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/users/me/payment-method": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get the saved card",
        "description": "The card auto-recharge and monthly statements are charged to, as the payment provider reports it. Only the brand, last four digits, expiry and issuing country are kept here. A card issued in Brazil is charged in Brazilian reais, every other card in US dollars.\n",
        "operationId": "getCurrentUserPaymentMethod",
        "responses": {
          "200": {
            "description": "The saved card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentMethod"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The account has no saved card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Start saving a card",
        "description": "A hosted page that saves a card for charges made without the payer present: auto-recharge and monthly statements. Send the caller to `checkout_url`; nothing is charged. A card saved this way replaces the one already saved.\n",
        "operationId": "createCurrentUserPaymentMethod",
        "responses": {
          "201": {
            "description": "The page to send the caller to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentMethodSetup"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "502": {
            "description": "The payment provider could not be reached (`payment_provider_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Card payments are not available on this deployment (`payments_not_configured`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Users"
        ],
        "summary": "Remove the saved card",
        "description": "Forgets the saved card and turns auto-recharge off.",
        "operationId": "deleteCurrentUserPaymentMethod",
        "responses": {
          "204": {
            "description": "The card is removed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The account has no saved card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Card payments are not available on this deployment (`payments_not_configured`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/users/me/auto-recharge": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get the auto-recharge setting",
        "description": "Whether the credit balance is topped up on the saved card, by how much and below what balance. `enabled` is false until a card is saved and the setting turned on.\n",
        "operationId": "getCurrentUserAutoRecharge",
        "responses": {
          "200": {
            "description": "The setting.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoRecharge"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "put": {
        "tags": [
          "Users"
        ],
        "summary": "Turn auto-recharge on",
        "description": "Top up `amount_usd` on the saved card whenever the credit balance falls below `below_usd`. Checked every 15 minutes, at most one charge an hour. A declined charge turns it off and emails the account.\n",
        "operationId": "setCurrentUserAutoRecharge",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AutoRechargeUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The setting.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoRecharge"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "The account has no saved card (`payment_method_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Users"
        ],
        "summary": "Turn auto-recharge off",
        "description": "Stops topping up the balance automatically. The saved card stays saved.\n",
        "operationId": "disableCurrentUserAutoRecharge",
        "responses": {
          "200": {
            "description": "The setting, now off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoRecharge"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/users/me/plan": {
      "put": {
        "tags": [
          "Users"
        ],
        "summary": "Change the plan",
        "description": "Choose `free`, `pro` or `business`.\n\nAn upgrade is charged for the price difference over the rest of the month, then takes effect; the month's statement nets that payment out. The saved card is charged at once, in its currency. With no saved card, or when the card needs authentication or is declined, nothing is charged and the answer carries a `checkout_url`: a hosted page that charges the upgrade and saves the card. The plan changes once it is paid; one left unpaid lapses at `expires_at`.\n\nA downgrade takes effect when the month ends, and `next_plan` names it until then. A contract plan is changed by asking us.\n",
        "operationId": "setCurrentUserPlan",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserPlanChangeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The plan in effect and the one coming.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserPlanChange"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "A paid plan with nothing to charge now and no saved card for its statement (`payment_method_required`), or a contract plan (`plan_by_contract`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "502": {
            "description": "The payment provider could not be reached (`payment_provider_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Card payments are not available on this deployment (`payments_not_configured`), or the exchange rate for a card charged in reais could not be read (`exchange_rate_unavailable`); the plan is unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/users/me/plan/quote": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Preview a plan change",
        "description": "What choosing `plan` would charge now: an upgrade's share of the rest of the month, else 0. Changes nothing.\n",
        "operationId": "getCurrentUserPlanQuote",
        "parameters": [
          {
            "name": "plan",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "free",
                "pro",
                "business"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The charge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserPlanQuote"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/projects/{project_id}/webhooks": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List webhooks",
        "description": "The endpoints registered in the project, newest first.",
        "operationId": "listWebhooks",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of webhooks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Create a webhook",
        "description": "Register an endpoint and subscribe it to one or more event types.\nThe response carries `secret` — the signing key, in plaintext. **This is the only time it is returned.** Store it where your receiver can read it; if you lose it, rotate rather than re-create, so the endpoint keeps its delivery history.\nA project holds at most 20 webhooks, inactive ones included; the 21st is refused with `409 webhook_limit_reached`.\nReturns `501` on a deployment with no credential-sealing key configured, since the secret could not then be stored safely.\n",
        "operationId": "createWebhook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook created. The signing secret is included, once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookWithSecret"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/WebhookLimitReached"
          },
          "501": {
            "$ref": "#/components/responses/SealingKeyNotConfigured"
          }
        }
      }
    },
    "/v1/projects/{project_id}/webhooks/{webhook_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/WebhookId"
        }
      ],
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Get a webhook",
        "description": "The endpoint, the events it is subscribed to and whether deliveries are attempted. The signing secret is not returned: it is shown once by [`POST /v1/projects/{project_id}/webhooks`](/docs/api/webhooks/create-webhook) and again by [`POST /v1/projects/{project_id}/webhooks/{webhook_id}:rotate-secret`](/docs/api/webhooks/rotate-webhook-secret).\n",
        "operationId": "getWebhook",
        "responses": {
          "200": {
            "description": "The webhook.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Update a webhook",
        "description": "Change the destination, the subscription, the label, or whether deliveries are attempted at all. At least one field is required.\nSetting `active: false` is the reversible half of `DELETE`: deliveries stop, the endpoint and its history stay. It is what to reach for while a receiver is being repaired.\n",
        "operationId": "updateWebhook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a webhook",
        "description": "Removes the endpoint and its delivery records. To stop deliveries while keeping the audit trail, `PATCH` it to `active: false` instead.\n",
        "operationId": "deleteWebhook",
        "responses": {
          "204": {
            "description": "Webhook deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/webhooks/{webhook_id}:rotate-secret": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/WebhookId"
        }
      ],
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Rotate the signing secret",
        "description": "Issues a new signing secret for the same endpoint and returns it — the second and last time a secret is ever returned. This is the `…:rotate-secret` action; the path segment is `{webhook_id}:rotate-secret`.\nThe change takes effect on the next delivery, including retries of deliveries already queued, so roll the new secret out to your receiver promptly. There is no overlap window in which both secrets verify.\n",
        "operationId": "rotateWebhookSecret",
        "responses": {
          "200": {
            "description": "A new signing secret was issued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookWithSecret"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "501": {
            "$ref": "#/components/responses/SealingKeyNotConfigured"
          }
        }
      }
    },
    "/v1/projects/{project_id}/webhook-deliveries": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List webhook deliveries",
        "description": "Every delivery attempted in the project, newest first — what was sent, where, how many times, and what came back. Filter by endpoint, by lifecycle status, or by event type.\nDeliveries are per (event, endpoint): an event matching two subscribed endpoints produces two rows, retried and observed independently.\nA `success` or `failed` delivery is deleted 30 days after it was created, and can no longer be read or redelivered. A `pending` one is kept until it settles.\n",
        "operationId": "listWebhookDeliveries",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "webhook_id",
            "in": "query",
            "required": false,
            "description": "Only deliveries addressed to this endpoint.",
            "schema": {
              "type": "string",
              "example": "whk_V1StGXR8Z5jdHi6B"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only deliveries in this state.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "success",
                "failed"
              ]
            }
          },
          {
            "name": "event_type",
            "in": "query",
            "required": false,
            "description": "Only deliveries of this event type.",
            "schema": {
              "type": "string",
              "example": "generation.completed"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of deliveries.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/webhook-deliveries/{delivery_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/DeliveryId"
        }
      ],
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Get a webhook delivery",
        "description": "One delivery, including the exact payload that was signed and sent.\n",
        "operationId": "getWebhookDelivery",
        "responses": {
          "200": {
            "description": "The delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/webhook-deliveries/{delivery_id}:redeliver": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "$ref": "#/components/parameters/DeliveryId"
        }
      ],
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Redeliver an event",
        "description": "Queue the same event at the same endpoint again — the recovery path for a delivery that failed, or one your receiver dropped. This is the `…:redeliver` action; the path segment is `{delivery_id}:redeliver`.\nA **new** delivery is created and returned; the original record is left untouched, because its attempt history is the evidence you redelivered on. The event's `id` is carried over unchanged, so a receiver deduping on the event sees the same event twice while one deduping on `X-Naturali-Delivery` sees a distinct delivery.\n",
        "operationId": "redeliverWebhookDelivery",
        "responses": {
          "202": {
            "description": "A new delivery was queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/projects/{project_id}/workflows": {
      "get": {
        "description": "Lists workflow definitions in a project.",
        "tags": [
          "Workflows"
        ],
        "summary": "List workflows",
        "operationId": "listWorkflows",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A list of workflows",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Workflow"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "description": "Creates a new workflow definition. The definition is statically validated.",
        "tags": [
          "Workflows"
        ],
        "summary": "Create a workflow",
        "operationId": "createWorkflow",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWorkflowRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Workflow created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Workflow"
                }
              }
            }
          },
          "400": {
            "description": "Bad request (invalid definition)"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "409": {
            "description": "A workflow with this name already exists"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of results to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of workflow versions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkflowVersion"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Workflow not found"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/workflows/{workflow_id}/versions/{version}": {
      "get": {
        "description": "Returns the exact state machine a given version describes. Every task records the version it entered on in `workflow_version` and runs on that machine for its whole life, so this is how you read the definition a task is actually being validated against — including a task whose workflow has been rewired since.\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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    },
    "/v1/projects/{project_id}/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"
          }
        }
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ]
    }
  },
  "components": {
    "schemas": {
      "ActivityEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "acte_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-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-naturali-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-naturali-ref": "agents",
            "type": "string",
            "nullable": true,
            "description": "Agent this actor is linked to (mutually exclusive with chatId)."
          },
          "chat_id": {
            "x-naturali-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"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "description": "Structured error. Every error response uses this shape, so `code` can be read without first checking the type of `error`.",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Machine-readable error code: lower_snake for an error naturali raises (`access_denied`), UPPER_SNAKE for one the runtime reports (`RESOURCE_NOT_FOUND`).",
                "example": "access_denied"
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation.",
                "example": "Your role in this project does not carry this action."
              },
              "details": {
                "type": "object",
                "additionalProperties": true,
                "description": "Structured context for an error naturali raises, such as the `resource` and `limit` of a `plan_limit_reached`."
              },
              "meta": {
                "type": "object",
                "description": "Structured context for an error the runtime reports."
              }
            }
          }
        }
      },
      "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 and knowledge search.\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"
        }
      },
      "Address": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "addr_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "identifier": {
            "type": "string",
            "example": "whatsapp:dm:5511999998888"
          },
          "display_name": {
            "type": "string",
            "nullable": true,
            "example": "Ana"
          },
          "actor_id": {
            "type": "string",
            "nullable": true,
            "description": "The runtime actor this address speaks as; null until it has needed one.",
            "example": "actor_V1StGXR8Z5jdHi6B"
          },
          "action": {
            "type": "string",
            "nullable": true,
            "enum": [
              "agent",
              "oneshot",
              "message",
              "silence",
              null
            ],
            "description": "This address's own decision, when it has one — beats every route.",
            "example": null
          },
          "agent_id": {
            "type": "string",
            "nullable": true,
            "example": null
          },
          "text": {
            "type": "string",
            "nullable": true,
            "example": null
          },
          "repeat": {
            "description": "Null unless `action` is `message`.",
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "every",
                  "once"
                ]
              },
              {
                "type": "object",
                "required": [
                  "after_seconds"
                ],
                "properties": {
                  "after_seconds": {
                    "type": "integer"
                  }
                }
              }
            ]
          },
          "language": {
            "type": "string",
            "nullable": true
          },
          "config": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Conversation config bag — media handling, persona overrides, the Discord allowlist, and how a `oneshot` answers.\n\n`discord.reply` picks the delivery: `react` (the default) adds `discord.reaction` — `\\u2705` unless named — to the message that triggered the run, `mention` answers beside it addressing whoever asked, `react_or_mention` reacts when the agent writes nothing and answers like `mention` when it writes a line, `thread` opens a thread and answers inside it, and `none` delivers nothing at all. A run that fails delivers nothing in every mode, so a reaction always means the work was done; there is no failure marker.\n\nA mention with nothing else in it is ignored unless the action says otherwise, for any action. `discord.use_replied_message: true` takes the text of the message it replies to, so replying to a message with only the mention files that message. `discord.empty_mention_text` answers any other bare mention with that fixed text, addressing the sender, without running the agent.\n\n`discord.forward_user_id: true` hands the sender's Discord user id to the agent's tools as the `discord_user_id` tool context, which a tool's `headers` read as `{{context:discord_user_id}}`. It is the id Discord reported for the message's author, never text the model wrote, so a tool can act for whoever sent it. Off unless set, because tool context reaches every tool the agent has.\n\nRead only for `oneshot`. A conversational `agent` in a guild *is* its thread — that is what its session is keyed on — so it always opens one.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "project_id",
          "identifier",
          "display_name",
          "actor_id",
          "action",
          "agent_id",
          "text",
          "repeat",
          "language",
          "config",
          "created_at",
          "updated_at"
        ]
      },
      "AddressActionSet": {
        "type": "object",
        "description": "`action` is required — one of `agent` / `message` / `silence`, or `null` to clear it. `agent_id` is required when it is `agent`, `text` when it is `message`; `repeat` is only valid alongside `action: message`.\n",
        "required": [
          "action"
        ],
        "properties": {
          "action": {
            "type": "string",
            "nullable": true,
            "enum": [
              "agent",
              "oneshot",
              "message",
              "silence",
              null
            ],
            "example": "agent",
            "description": "What an inbound message becomes.\n\n`agent` is a dialogue: the identity behind the message gets a session, and every later message on it continues the same one. `oneshot` runs the agent once and keeps nothing — no address, no actor, no session, no conversation — so each message is answered on its own. Choose it for work that is filed rather than discussed; choose `agent` when the answer depends on what came before.\n\n`message` delivers fixed text without running an agent, and `silence` answers nothing.\n"
          },
          "agent_id": {
            "type": "string",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "text": {
            "type": "string",
            "example": "Subscribe at https://example.com/pricing"
          },
          "repeat": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "every",
                  "once"
                ]
              },
              {
                "type": "object",
                "required": [
                  "after_seconds"
                ],
                "properties": {
                  "after_seconds": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              }
            ],
            "example": "every"
          },
          "language": {
            "type": "string",
            "example": "en-US"
          },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "description": "Conversation config bag — media handling, persona overrides, the Discord allowlist, and how a `oneshot` answers.\n\n`discord.reply` picks the delivery: `react` (the default) adds `discord.reaction` — `\\u2705` unless named — to the message that triggered the run, `mention` answers beside it addressing whoever asked, `react_or_mention` reacts when the agent writes nothing and answers like `mention` when it writes a line, `thread` opens a thread and answers inside it, and `none` delivers nothing at all. A run that fails delivers nothing in every mode, so a reaction always means the work was done; there is no failure marker.\n\nA mention with nothing else in it is ignored unless the action says otherwise, for any action. `discord.use_replied_message: true` takes the text of the message it replies to, so replying to a message with only the mention files that message. `discord.empty_mention_text` answers any other bare mention with that fixed text, addressing the sender, without running the agent.\n\n`discord.forward_user_id: true` hands the sender's Discord user id to the agent's tools as the `discord_user_id` tool context, which a tool's `headers` read as `{{context:discord_user_id}}`. It is the id Discord reported for the message's author, never text the model wrote, so a tool can act for whoever sent it. Off unless set, because tool context reaches every tool the agent has.\n\nRead only for `oneshot`. A conversational `agent` in a guild *is* its thread — that is what its session is keyed on — so it always opens one.\n"
          },
          "display_name": {
            "type": "string",
            "description": "Set only when the address is created by this call.",
            "example": "Ana"
          }
        }
      },
      "AddressList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Address"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "example": null
          }
        }
      },
      "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-naturali-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-naturali-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-naturali-ref": "model-routes"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Display name"
          },
          "instructions": {
            "type": "string",
            "nullable": true,
            "description": "System instructions guiding behavior"
          },
          "model": {
            "type": "string",
            "nullable": true,
            "description": "Model identifier"
          },
          "tool_bindings": {
            "type": "array",
            "nullable": true,
            "items": {
              "$ref": "#/components/schemas/ToolBinding"
            },
            "description": "Tools attached to this agent, one binding object per tool — the canonical attachment field. See [Tool Bindings](/docs/modules/agents#tool-bindings)."
          },
          "max_steps": {
            "type": "integer",
            "nullable": true,
            "description": "Maximum agent loop steps before stopping. The budget bounds a **turn**: a generation that pauses at `requires_action` and resumes after `submit-tool-outputs` continues the same turn and spends what is left of it, so a turn that arrives with nothing left completes with `stop_reason: \"max_steps\"` instead of calling the model again."
          },
          "tool_choice": {
            "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-naturali-ref": "tools",
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "Subset of the bound tools that are active"
          },
          "guardrail_ids": {
            "x-naturali-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 runtime 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-naturali-ref": "memory-stores",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "document_ids": {
                "x-naturali-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-naturali-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 a cache breakpoint at the end of the turn's static prefix — the tool definitions and the instructions together — so a provider that caches by explicit breakpoint reads that prefix back on every later step of the turn and every later turn of the session instead of being charged for it again. 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. An agent with no `instructions` has no block to mark and caches nothing. Cache reads are reported as `cached_tokens` and cache writes as `cache_write_tokens` on usage.",
            "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."
          },
          "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-naturali-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-naturali-ref": "agents",
            "type": "string",
            "description": "Public ID of the agent this version belongs to",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "version": {
            "type": "integer",
            "description": "The archived version number",
            "example": 1
          },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "description": "The agent's configuration as it stood at this version: every mutable field of the `Agent` schema (`instructions`, `model`, `tool_bindings`, `max_steps`, `tool_choice`, `stop_conditions`, `active_tool_ids`, `step_rules`, `boundary_policy`, `temperature`, `knowledge_config`, `output_schema`, `max_context_messages`, `single_session_per_actor`, `on_approval_expiry`, `guardrail_ids`, `ai_provider_id`, `model_route_id`, `name`), and none of its identity or bookkeeping fields (`id`, `project_id`, `version`, `active_release`, timestamps).\n\nDeliberately open rather than a fixed schema: an archive written by an earlier release of the runtime 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-naturali-ref": "eval-runs",
            "type": "string",
            "nullable": true,
            "description": "The eval run that cleared the release's `promotion_gate` when this version was promoted. Null for every version that did not go live through a gated promotion — which is most of them.",
            "example": "evrun_V1StGXR8Z5jdHi6B"
          },
          "created_by": {
            "x-naturali-ref": "users",
            "type": "string",
            "nullable": true,
            "description": "Public ID of the user whose action produced this version. A formation apply is attributed to the project's owning identity; null when no principal could be resolved."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RestoreAgentVersionRequest": {
        "type": "object",
        "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-naturali-ref": "evals",
            "type": "string",
            "nullable": true,
            "description": "Eval to gate promotion on. It must belong to this project and evaluate this agent; anything else is a `400`. Omit it, or send null, for a rollout that can be promoted at will.",
            "example": "eval_V1StGXR8Z5jdHi6B"
          }
        }
      },
      "ToolBinding": {
        "type": "object",
        "description": "One agent↔tool attachment. Exactly one of `tool_id` (persisted tool reference) or `tool` (inline ephemeral definition) per entry. Tool-call gating is owned by [Guardrails](/docs/modules/guardrails), attached via `guardrail_ids` on the project, agent, or tool — not on the binding.",
        "properties": {
          "tool_id": {
            "x-naturali-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"
          }
        }
      },
      "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": {
          "ai_provider_id": {
            "x-naturali-ref": "ai-providers",
            "type": "string",
            "description": "Public ID of the AI provider to pin. Mutually exclusive with `model_route_id`."
          },
          "model_route_id": {
            "x-naturali-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-naturali-ref": "tools",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "guardrail_ids": {
            "x-naturali-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-naturali-ref": "memory-stores",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "document_ids": {
                "x-naturali-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-naturali-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 a cache breakpoint at the end of the turn's static prefix — the tool definitions and the instructions together — so a provider that caches by explicit breakpoint reads that prefix back on every later step of the turn and every later turn of the session instead of being charged for it again. 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. An agent with no `instructions` has no block to mark and caches nothing. Cache reads are reported as `cached_tokens` and cache writes as `cache_write_tokens` on usage.",
            "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."
          },
          "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-naturali-ref": "ai-providers",
            "type": "string",
            "nullable": true
          },
          "model_route_id": {
            "x-naturali-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-naturali-ref": "tools",
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            }
          },
          "guardrail_ids": {
            "x-naturali-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-naturali-ref": "memory-stores",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "document_ids": {
                "x-naturali-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-naturali-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 a cache breakpoint at the end of the turn's static prefix — the tool definitions and the instructions together — so a provider that caches by explicit breakpoint reads that prefix back on every later step of the turn and every later turn of the session instead of being charged for it again. 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. An agent with no `instructions` has no block to mark and caches nothing. Cache reads are reported as `cached_tokens` and cache writes as `cache_write_tokens` on usage.",
            "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."
          },
          "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-naturali-tool-unsupported": true,
            "description": "When true the response is an SSE stream"
          },
          "trace_id": {
            "x-naturali-ref": "traces",
            "type": "string",
            "x-naturali-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-naturali-ref": "traces",
            "type": "string",
            "nullable": true,
            "x-naturali-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-naturali-ref": "traces",
            "type": "string",
            "nullable": true,
            "x-naturali-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-naturali-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-Naturali-Context-<key>` headers on every `http` and `mcp` tool call in this generation. The header name is the deployment's configured context prefix (`X-Naturali-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-naturali-ref": "memory-stores",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "document_ids": {
                "x-naturali-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-naturali-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 `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-naturali-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 /v1/projects/{project_id}/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-naturali-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-naturali-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 runtime 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": {}
            }
          }
        }
      },
      "CreateToolRequest": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Tool name"
          },
          "type": {
            "type": "string",
            "enum": [
              "http",
              "client",
              "mcp",
              "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 `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-Naturali-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.\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-naturali-ref": "guardrails",
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "Guardrails attached at the tool scope."
          }
        }
      },
      "ToolExecuteConfig": {
        "type": "object",
        "nullable": true,
        "additionalProperties": false,
        "description": "Execution config for http tools. Supported fields: `url` (required), `method` (default `POST`), `headers`, and `body_mode`. The `url` may contain `{paramName}` placeholders (e.g. `/users/{userId}`) that are replaced at call time with the corresponding tool argument value (URL-encoded). Arguments consumed as path parameters are excluded from the query string and request body. `body_mode` is `json` (default) or `multipart`. In `multipart` mode the merged tool arguments are sent as a `multipart/form-data` body: scalar fields become plain form fields and a field shaped like `{ content_type, filename, data_base64 }` is decoded from base64 and attached as a file part (the hardcoded `Content-Type: application/json` is dropped so `fetch` sets the multipart boundary itself).\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 written as a **literal** is stored and still sent on every call, but it is masked on every read: `secret_access_key`, `session_token` and `credentials` under `auth`, and any credential-named `headers` value (`Authorization`, `Cookie`, or a name containing `api-key`/`token`/`secret`/`password`), come back as `{\"no_echo\": true}`. The mask is 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-Naturali-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`."
          },
          "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 accepts a `{{secret:...}}` reference and should carry one, since a literal is stored as written and masked on read.",
        "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. Masked on read as `{\"no_echo\": true}` when written as a literal."
          },
          "session_token": {
            "type": "string",
            "nullable": true,
            "description": "`aws_sigv4`: the session token of a temporary credential. Masked on read, like `secret_access_key`."
          },
          "credentials": {
            "type": "string",
            "nullable": true,
            "description": "`gcp_service_account`: the service account key file JSON, as a string. Masked on read, 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` is masked on read, 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."
          }
        }
      },
      "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
      },
      "NullableMetadataBag": {
        "type": "object",
        "nullable": true,
        "description": "A `MetadataBag` on a field where `null` is meaningful — a full-replacement update that clears the bag, or a record whose bag was never set.",
        "example": {
          "author": "John",
          "revision": 2
        }
      },
      "ProviderPrice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public ID of the price row",
            "example": "price_V1StGXR8Z5jdHi6B"
          },
          "ai_provider_id": {
            "x-naturali-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": [
          "provider",
          "models"
        ],
        "properties": {
          "provider": {
            "type": "string",
            "description": "The provider slug the listing came from",
            "example": "vertex"
          },
          "models": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "description": "The provider-native model id, ready to use as `default_model` or an agent's `model`\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": "Leading characters of the raw key, for identification.",
            "example": "nat_sk_a1b2c3"
          },
          "scope": {
            "type": "string",
            "enum": [
              "project",
              "account"
            ],
            "description": "Whether the key is scoped to one project or the whole account.",
            "example": "project"
          },
          "project_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "projects",
            "description": "The project this key is scoped to; null for account-scoped keys.",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "capabilities": {
            "type": "array",
            "description": "Capabilities granted to this key. Empty means it inherits the creator's.",
            "items": {
              "type": "string"
            },
            "example": [
              "agents:read",
              "sessions:write"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-17T00:00:00.000Z"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the key was last presented; null if never used.",
            "example": "2026-07-17T09:12:00.000Z"
          }
        },
        "required": [
          "id",
          "name",
          "key_prefix",
          "scope",
          "capabilities",
          "created_at"
        ]
      },
      "ApiKeyCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiKeyRecord"
          },
          {
            "type": "object",
            "required": [
              "key"
            ],
            "properties": {
              "key": {
                "type": "string",
                "description": "The raw API key value (only returned once, at creation or rotation). Use as the Bearer token.",
                "example": "nat_sk_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
              }
            }
          }
        ]
      },
      "ApiKeyCreate": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Key name for identification.",
            "example": "CI/CD Pipeline"
          },
          "project_id": {
            "type": "string",
            "x-naturali-ref": "projects",
            "description": "Project to scope the key to. Omit for an account-scoped key.",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "capabilities": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Capabilities to grant. Omit to inherit the creator's capabilities.",
            "example": [
              "agents:read",
              "sessions:write"
            ]
          }
        }
      },
      "ApiKeyUpdate": {
        "type": "object",
        "description": "At least one field must be present. Scope is immutable.",
        "minProperties": 1,
        "properties": {
          "name": {
            "type": "string",
            "example": "Renamed Key"
          },
          "capabilities": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Replace the capability set (empty array clears all).",
            "example": [
              "agents:read"
            ]
          }
        }
      },
      "ApiKeyList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKeyRecord"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "example": null
          }
        }
      },
      "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-naturali-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 action name) for `tool_call`-origin items — always present there, even for single-action tools. Omitted for `node`-origin items, whose downstream execution is wired by a separate `tool` node in the graph."
              },
              "arguments": {
                "type": "object"
              }
            }
          },
          "reasoning": {
            "type": "string",
            "nullable": true,
            "description": "The proposing agent's rationale"
          },
          "evidence": {
            "type": "object",
            "nullable": true,
            "description": "Supporting structured data"
          },
          "predicted_impact": {
            "type": "string",
            "nullable": true,
            "description": "Expected execution effect"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Server-enforced hard gate; the item can never execute after this"
          },
          "dedup_key": {
            "type": "string",
            "nullable": true,
            "description": "Set on tool-call items to suppress duplicate proposals"
          },
          "orchestration_run_id": {
            "type": "string",
            "nullable": true,
            "description": "Originating orchestration run (node producer)"
          },
          "node_id": {
            "type": "string",
            "nullable": true,
            "description": "Originating node id within the run's graph"
          },
          "generation_id": {
            "type": "string",
            "nullable": true,
            "description": "Originating generation (tool-call producer)"
          },
          "session_id": {
            "type": "string",
            "nullable": true,
            "description": "Session the originating generation ran in (tool-call producer)"
          },
          "agent_id": {
            "type": "string",
            "nullable": true,
            "description": "Proposing agent"
          },
          "task_id": {
            "type": "string",
            "nullable": true,
            "description": "Gated task (task_transition producer)"
          },
          "task_transition": {
            "type": "string",
            "nullable": true,
            "description": "Transition fired on approval (task_transition producer)"
          },
          "policy_version": {
            "type": "string",
            "nullable": true
          },
          "previous_item_id": {
            "type": "string",
            "nullable": true,
            "description": "Prior item's ID when this proposal was re-filed after an earlier matching item (same dedup_key) had been rejected",
            "example": "apr_V1StGXR8Z5jdHi6B"
          },
          "resolved_by": {
            "type": "string",
            "nullable": true,
            "description": "Resolving user's public ID; null on expiry"
          },
          "resolution_reason": {
            "type": "string",
            "nullable": true,
            "description": "Required on rejection"
          },
          "edited_arguments": {
            "type": "object",
            "nullable": true,
            "description": "Set on edit-then-approve"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AssistantGrant": {
        "type": "object",
        "description": "One channel identity authorized to operate one naturali account through the Assistant.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public grant ID (agr_ prefix).",
            "example": "agr_V1StGXR8Z5jdHi6B"
          },
          "channel": {
            "$ref": "#/components/schemas/AssistantChannel"
          },
          "identifier": {
            "type": "string",
            "description": "The channel identity, rendered with its channel prefix — a Discord user id, a WhatsApp phone number, a Slack team and user. One identity operates one account.\n",
            "example": "discord:112233445566778899"
          },
          "display_name": {
            "type": "string",
            "nullable": true,
            "description": "The identity as its channel reported it, when it reported one — what a person recognizes instead of an opaque id. Null when the channel offers nothing better than the identifier.\n",
            "example": "ana"
          },
          "scopes": {
            "type": "array",
            "description": "What the Assistant may do on the account. Deliberately coarse: the consent for a specific dangerous action is asked for when it happens, through the approval queue, rather than pre-granted here.\n",
            "items": {
              "$ref": "#/components/schemas/AssistantScope"
            },
            "example": [
              "read"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "revoked"
            ],
            "description": "Whether the Assistant currently answers this identity.",
            "example": "active"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the Assistant last acted under this grant; null if never.",
            "example": "2026-07-17T09:12:00.000Z"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-17T00:00:00.000Z"
          }
        },
        "required": [
          "id",
          "channel",
          "identifier",
          "display_name",
          "scopes",
          "status",
          "last_used_at",
          "created_at"
        ]
      },
      "AssistantChannel": {
        "type": "string",
        "enum": [
          "discord",
          "whatsapp",
          "slack"
        ],
        "description": "The channel the linked identity lives on. The app is never a value here: a signed-in web session already *is* the identity, so it needs no grant.\n",
        "example": "discord"
      },
      "AssistantScope": {
        "type": "string",
        "enum": [
          "read",
          "manage"
        ],
        "description": "`read` lists and inspects projects, agents, channels, knowledge and usage. `manage` additionally mutates, still subject to the approval queue for destructive operations. A link currently grants `read`.\n",
        "example": "read"
      },
      "AssistantLinkPreview": {
        "type": "object",
        "description": "What a pending link would do, resolved from its token without consuming it.\n",
        "properties": {
          "channel": {
            "$ref": "#/components/schemas/AssistantChannel"
          },
          "identifier": {
            "type": "string",
            "description": "The channel identity this token would link, with its channel prefix.",
            "example": "discord:112233445566778899"
          },
          "display_name": {
            "type": "string",
            "nullable": true,
            "description": "The identity as its channel reported it, for the confirmation screen.",
            "example": "ana"
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssistantScope"
            },
            "description": "What redeeming this token would allow.",
            "example": [
              "read"
            ]
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the token stops being redeemable. Links are short-lived by design.",
            "example": "2026-07-17T00:10:00.000Z"
          }
        },
        "required": [
          "channel",
          "identifier",
          "display_name",
          "scopes",
          "expires_at"
        ]
      },
      "AssistantLinkRedeem": {
        "type": "object",
        "required": [
          "token"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "The single-use token from the link the Assistant sent on the channel.",
            "example": "alt_9f8e7d6c5b4a39281706"
          }
        }
      },
      "AssistantMessage": {
        "type": "object",
        "required": [
          "id",
          "role",
          "text"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "doc_V1StGXR8Z5jdHi6B"
          },
          "role": {
            "type": "string",
            "enum": [
              "user",
              "assistant"
            ],
            "example": "assistant"
          },
          "text": {
            "type": "string",
            "example": "You have two projects."
          }
        }
      },
      "AssistantMessageCreate": {
        "type": "object",
        "required": [
          "text"
        ],
        "additionalProperties": false,
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "example": "List my projects."
          }
        }
      },
      "AssistantMessageList": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssistantMessage"
            }
          }
        }
      },
      "AssistantGrantList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssistantGrant"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "example": null
          }
        }
      },
      "AuditEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "audit_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-ref": "projects",
            "type": "string",
            "nullable": true,
            "description": "Project the action targeted; null for global actions"
          },
          "principal_type": {
            "type": "string",
            "nullable": true,
            "enum": [
              "user",
              "api_key",
              null
            ],
            "description": "Principal kind; null for platform-originated entries (those are identified by their `action`, e.g. `quotas:MonitorBreach`)"
          },
          "principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of the principal (`user_…` or `key_…`); null for platform-originated entries"
          },
          "action": {
            "type": "string",
            "description": "The permission-action string that authorized the request",
            "example": "secrets:DeleteSecret"
          },
          "resource_srn": {
            "type": "string",
            "nullable": true,
            "description": "SRN the action targeted (type-level `srn:{project}:{type}:*` on creates)",
            "example": "srn:proj_V1StGXR8Z5jdHi6B:secret:sec_V1StGXR8Z5jdHi6B"
          },
          "resource_public_id": {
            "type": "string",
            "nullable": true,
            "description": "Target resource public id (from the SRN, or the response body on creates)"
          },
          "status": {
            "type": "integer",
            "description": "HTTP status of the response",
            "example": 200
          },
          "request_id": {
            "type": "string",
            "nullable": true,
            "description": "Per-request correlation id (also returned in the X-Request-Id header)"
          },
          "ip": {
            "type": "string",
            "nullable": true
          },
          "user_agent": {
            "type": "string",
            "nullable": true
          },
          "detail": {
            "type": "object",
            "nullable": true,
            "description": "Kind-specific payload. Multi-check routes record the remaining checks under `additional_checks`. Platform-originated entries set a `detail.kind` discriminator, e.g. `quota_monitor_breach` or `guardrail_evaluation`.",
            "additionalProperties": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SignInCodeRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "example": "ana@acme.com"
          }
        }
      },
      "SignInCodeVerify": {
        "type": "object",
        "required": [
          "email",
          "code"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "example": "ana@acme.com"
          },
          "code": {
            "type": "string",
            "description": "The code from the email, as sent — digits only.",
            "pattern": "^[0-9]{4,12}$",
            "example": "481902"
          }
        }
      },
      "GoogleSignIn": {
        "type": "object",
        "required": [
          "id_token"
        ],
        "properties": {
          "id_token": {
            "type": "string",
            "description": "The `credential` Google Identity Services returns to the page.",
            "example": "eyJhbGciOiJSUzI1NiIs..."
          }
        }
      },
      "Acknowledgement": {
        "type": "object",
        "description": "A deliberately uninformative confirmation. The wording is identical for a known and an unknown address so the response cannot be used to enumerate accounts.\n",
        "properties": {
          "message": {
            "type": "string",
            "example": "If the address is registered, an email has been sent."
          }
        }
      },
      "AuthSession": {
        "type": "object",
        "description": "A short-lived access JWT plus a rotating refresh token. The refresh token is also set as an httpOnly, SameSite=Lax cookie scoped to `/v1/auth` (Secure whenever the connection is https), which is what carries a browser session across a reload; the body copy is for clients with no cookie jar, such as the CLI.\n",
        "properties": {
          "token_type": {
            "type": "string",
            "enum": [
              "Bearer"
            ],
            "example": "Bearer"
          },
          "access_token": {
            "type": "string",
            "description": "Short-lived access JWT. Send as `Authorization: Bearer <token>`.",
            "example": "eyJhbGciOiJIUzI1NiJ9..."
          },
          "expires_in": {
            "type": "integer",
            "description": "Access-token lifetime in seconds.",
            "example": 900
          },
          "refresh_token": {
            "type": "string",
            "description": "Single-use refresh token, rotated on every refresh.",
            "example": "rt_9f2StGXR8Z5jdHi6B..."
          },
          "user": {
            "$ref": "#/components/schemas/User"
          }
        },
        "required": [
          "token_type",
          "access_token",
          "expires_in",
          "user"
        ]
      },
      "RefreshRequest": {
        "type": "object",
        "description": "Send an empty object from a browser — the httpOnly `refresh_token` cookie is presented instead, and takes over when the body omits the field.\n",
        "properties": {
          "refresh_token": {
            "type": "string",
            "example": "rt_9f2StGXR8Z5jdHi6B..."
          }
        }
      },
      "LogoutRequest": {
        "type": "object",
        "properties": {
          "refresh_token": {
            "type": "string",
            "description": "The refresh token to revoke; defaults to the current session's.",
            "example": "rt_9f2StGXR8Z5jdHi6B..."
          },
          "all": {
            "type": "boolean",
            "default": false,
            "description": "Revoke every active session for the user."
          }
        }
      },
      "User": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public user ID (user_ prefix).",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "email": {
            "type": "string",
            "format": "email",
            "example": "ana@acme.com"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "example": "Ana Silva"
          },
          "email_verified": {
            "type": "boolean",
            "example": true
          },
          "roles": {
            "type": "array",
            "description": "Roles on naturali itself, not on a project. Empty for almost every account; `admin` marks a naturali operator. Changed only through `PUT /v1/admin/users/{user_id}/roles`, by a caller holding the role that grants each one.",
            "items": {
              "type": "string",
              "enum": [
                "admin"
              ]
            },
            "example": []
          },
          "usage_alerts": {
            "type": "boolean",
            "description": "Whether the account is emailed when its model credit, runs or indexed storage reaches 75%, 90% and 100%. On for a new account.",
            "example": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-17T00:00:00.000Z"
          }
        },
        "required": [
          "id",
          "email",
          "email_verified",
          "roles",
          "usage_alerts",
          "created_at"
        ]
      },
      "Chain": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "chain_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-ref": "projects",
            "type": "string"
          },
          "agent_id": {
            "x-naturali-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 /v1/projects/{project_id}/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"
          }
        }
      },
      "ChannelSurface": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Stable, wire-visible id, unique within the kind.",
            "example": "guild_thread"
          },
          "title": {
            "type": "string",
            "example": "Server thread"
          },
          "shared": {
            "type": "boolean",
            "description": "Several humans share one conversation here (a thread, a Slack channel).",
            "example": true
          }
        },
        "required": [
          "name",
          "title",
          "shared"
        ]
      },
      "ChannelPredicate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "example": "guild_id"
          },
          "type": {
            "type": "string",
            "description": "The value shape this predicate's `match` entry expects.",
            "example": "string"
          }
        },
        "required": [
          "name",
          "type"
        ]
      },
      "ChannelKind": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "whatsapp",
              "discord"
            ],
            "example": "discord"
          },
          "surfaces": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChannelSurface"
            }
          },
          "predicates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChannelPredicate"
            }
          }
        },
        "required": [
          "kind",
          "surfaces",
          "predicates"
        ]
      },
      "ChannelKindList": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChannelKind"
            }
          }
        }
      },
      "ChannelRoute": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "route_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "channel_id": {
            "type": "string",
            "example": "chan_V1StGXR8Z5jdHi6B"
          },
          "surface": {
            "type": "string",
            "nullable": true,
            "description": "The surface this route matches, or null for \"any\" (`GET /v1/channel-kinds`).",
            "example": "guild_thread"
          },
          "match": {
            "type": "object",
            "additionalProperties": true,
            "description": "The predicate bag, ANDed. `{}` matches every message on this surface.",
            "example": {
              "guild_id": "9988776655"
            }
          },
          "priority": {
            "type": "integer",
            "description": "Tiebreak only, after specificity and `surface`.",
            "example": 0
          },
          "action": {
            "type": "string",
            "enum": [
              "agent",
              "oneshot",
              "message",
              "silence"
            ],
            "example": "agent",
            "description": "What an inbound message becomes.\n\n`agent` is a dialogue: the identity behind the message gets a session, and every later message on it continues the same one. `oneshot` runs the agent once and keeps nothing — no address, no actor, no session, no conversation — so each message is answered on its own. Choose it for work that is filed rather than discussed; choose `agent` when the answer depends on what came before.\n\n`message` delivers fixed text without running an agent, and `silence` answers nothing.\n"
          },
          "agent_id": {
            "type": "string",
            "nullable": true,
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "text": {
            "type": "string",
            "nullable": true,
            "example": "Subscribe at https://example.com/pricing"
          },
          "repeat": {
            "description": "Null unless `action` is `message`.",
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "every",
                  "once"
                ]
              },
              {
                "type": "object",
                "required": [
                  "after_seconds"
                ],
                "properties": {
                  "after_seconds": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              }
            ],
            "example": "every"
          },
          "language": {
            "type": "string",
            "nullable": true,
            "example": "en-US"
          },
          "config": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Conversation config bag — media handling, persona overrides, the Discord allowlist, and how a `oneshot` answers.\n\n`discord.reply` picks the delivery: `react` (the default) adds `discord.reaction` — `\\u2705` unless named — to the message that triggered the run, `mention` answers beside it addressing whoever asked, `react_or_mention` reacts when the agent writes nothing and answers like `mention` when it writes a line, `thread` opens a thread and answers inside it, and `none` delivers nothing at all. A run that fails delivers nothing in every mode, so a reaction always means the work was done; there is no failure marker.\n\nA mention with nothing else in it is ignored unless the action says otherwise, for any action. `discord.use_replied_message: true` takes the text of the message it replies to, so replying to a message with only the mention files that message. `discord.empty_mention_text` answers any other bare mention with that fixed text, addressing the sender, without running the agent.\n\n`discord.forward_user_id: true` hands the sender's Discord user id to the agent's tools as the `discord_user_id` tool context, which a tool's `headers` read as `{{context:discord_user_id}}`. It is the id Discord reported for the message's author, never text the model wrote, so a tool can act for whoever sent it. Off unless set, because tool context reaches every tool the agent has.\n\nRead only for `oneshot`. A conversational `agent` in a guild *is* its thread — that is what its session is keyed on — so it always opens one.\n"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ],
            "example": "active"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "project_id",
          "channel_id",
          "surface",
          "match",
          "priority",
          "action",
          "agent_id",
          "text",
          "repeat",
          "language",
          "config",
          "status",
          "created_at",
          "updated_at"
        ]
      },
      "ChannelRouteWrite": {
        "type": "object",
        "required": [
          "action"
        ],
        "properties": {
          "surface": {
            "type": "string",
            "nullable": true,
            "example": "guild_thread"
          },
          "match": {
            "type": "object",
            "additionalProperties": true,
            "example": {
              "guild_id": "9988776655"
            }
          },
          "priority": {
            "type": "integer",
            "default": 0
          },
          "action": {
            "type": "string",
            "enum": [
              "agent",
              "oneshot",
              "message",
              "silence"
            ],
            "example": "agent",
            "description": "What an inbound message becomes.\n\n`agent` is a dialogue: the identity behind the message gets a session, and every later message on it continues the same one. `oneshot` runs the agent once and keeps nothing — no address, no actor, no session, no conversation — so each message is answered on its own. Choose it for work that is filed rather than discussed; choose `agent` when the answer depends on what came before.\n\n`message` delivers fixed text without running an agent, and `silence` answers nothing.\n"
          },
          "agent_id": {
            "type": "string",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "text": {
            "type": "string",
            "example": "Subscribe at https://example.com/pricing"
          },
          "repeat": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "every",
                  "once"
                ]
              },
              {
                "type": "object",
                "required": [
                  "after_seconds"
                ],
                "properties": {
                  "after_seconds": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              }
            ],
            "example": "every"
          },
          "language": {
            "type": "string",
            "example": "en-US"
          },
          "config": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Conversation config bag — media handling, persona overrides, the Discord allowlist, and how a `oneshot` answers.\n\n`discord.reply` picks the delivery: `react` (the default) adds `discord.reaction` — `\\u2705` unless named — to the message that triggered the run, `mention` answers beside it addressing whoever asked, `react_or_mention` reacts when the agent writes nothing and answers like `mention` when it writes a line, `thread` opens a thread and answers inside it, and `none` delivers nothing at all. A run that fails delivers nothing in every mode, so a reaction always means the work was done; there is no failure marker.\n\nA mention with nothing else in it is ignored unless the action says otherwise, for any action. `discord.use_replied_message: true` takes the text of the message it replies to, so replying to a message with only the mention files that message. `discord.empty_mention_text` answers any other bare mention with that fixed text, addressing the sender, without running the agent.\n\n`discord.forward_user_id: true` hands the sender's Discord user id to the agent's tools as the `discord_user_id` tool context, which a tool's `headers` read as `{{context:discord_user_id}}`. It is the id Discord reported for the message's author, never text the model wrote, so a tool can act for whoever sent it. Off unless set, because tool context reaches every tool the agent has.\n\nRead only for `oneshot`. A conversational `agent` in a guild *is* its thread — that is what its session is keyed on — so it always opens one.\n"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ],
            "default": "active"
          }
        }
      },
      "ChannelRouteList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChannelRoute"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "example": null
          }
        }
      },
      "Channel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public channel ID (chan_ prefix).",
            "example": "chan_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "channel": {
            "type": "string",
            "enum": [
              "whatsapp",
              "discord"
            ],
            "example": "whatsapp"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ],
            "example": "active"
          },
          "credential_source": {
            "type": "string",
            "enum": [
              "byot",
              "embedded_signup"
            ],
            "description": "How the credential was obtained.",
            "example": "byot"
          },
          "phone_number_id": {
            "type": "string",
            "nullable": true,
            "description": "WhatsApp phone number id; inbound webhooks route on this. Null for non-WhatsApp channels.",
            "example": "109999999999999"
          },
          "waba_id": {
            "type": "string",
            "nullable": true,
            "description": "WhatsApp Business Account id, when known.",
            "example": "104444444444444"
          },
          "application_id": {
            "type": "string",
            "nullable": true,
            "description": "Discord application id — the tenant's app, one channel per app. The gateway worker holds one connection per channel. Null for non-Discord channels.\n",
            "example": "1290000000000000000"
          },
          "modes": {
            "$ref": "#/components/schemas/DiscordModes"
          },
          "default": {
            "$ref": "#/components/schemas/ChannelDefaultAction"
          },
          "has_credential": {
            "type": "boolean",
            "description": "Whether an access token is on file (its value is never returned).",
            "example": true
          },
          "gateway_error": {
            "type": "object",
            "nullable": true,
            "description": "Why Discord refused this channel's gateway connection, when it did: the worker stops retrying, so the channel stays silent until a `PATCH` changes `bot_token`, `modes` or `status`, which clears it. Null when there is none, and for non-Discord channels.\n",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_bot_token",
                  "disallowed_intents",
                  "gateway_rejected"
                ],
                "description": "`invalid_bot_token` — the token was reset or revoked. `disallowed_intents` — `mention_threads` needs the privileged Message Content intent enabled on the Discord application.\n"
              },
              "close_code": {
                "type": "integer",
                "description": "The gateway close code Discord sent.",
                "example": 4004
              },
              "at": {
                "type": "string",
                "format": "date-time"
              }
            },
            "required": [
              "code",
              "close_code",
              "at"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-23T00:00:00.000Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-23T00:00:00.000Z"
          }
        },
        "required": [
          "id",
          "project_id",
          "channel",
          "status",
          "credential_source",
          "phone_number_id",
          "waba_id",
          "application_id",
          "modes",
          "default",
          "has_credential",
          "gateway_error",
          "created_at",
          "updated_at"
        ]
      },
      "ChannelCreate": {
        "type": "object",
        "description": "Connect a channel. The required fields depend on `channel`:\n* **whatsapp** — `phone_number_id`, plus a credential by\n  `credential_source`: `byot` (default) requires `access_token`;\n  `embedded_signup` requires `code` and `waba_id`.\n\n* **discord** — `application_id` and `bot_token` (write-only), plus\n  the optional `modes` opting into the gateway flows. Discord runs over\n  the Gateway, so the bot token is kept in naturali's encrypted store\n  (the worker must present it to authenticate its WebSocket) rather than\n  as a secret on the runtime; deployments without `CHANNEL_TOKEN_KEY` configured\n  return `501`.\n\nNo credential value is ever returned. `default` is required (CHANNELS-ROUTING.md §3.3.1) — connecting a channel without saying what it does when nothing else matches is the misconfiguration, so it is caught here rather than discovered as silence in production.\n",
        "required": [
          "default"
        ],
        "properties": {
          "idempotency_key": {
            "type": "string",
            "maxLength": 255,
            "description": "Deduplication key, unique within your account, that makes a retry of an ambiguous failure safe. The first request under a key performs the write and answers `201`; any later request carrying the same key answers `200` with that same result, so a timeout you cannot interpret can simply be retried.\n\nThe key is claimed for as long as the record exists and never silently expires. Reusing one with a different request body is `409 idempotency_key_reused` — a key names one request, so changing the body and keeping the key is a bug rather than a retry. A retry that arrives while the original is still in flight is `409 idempotency_request_in_progress`.",
            "example": "signup-2026-09-18-acct-42"
          },
          "default": {
            "$ref": "#/components/schemas/ChannelDefaultActionInput"
          },
          "channel": {
            "type": "string",
            "enum": [
              "whatsapp",
              "discord"
            ],
            "default": "whatsapp",
            "example": "whatsapp"
          },
          "phone_number_id": {
            "type": "string",
            "description": "Required for `whatsapp`.",
            "example": "109999999999999"
          },
          "credential_source": {
            "type": "string",
            "enum": [
              "byot",
              "embedded_signup"
            ],
            "default": "byot",
            "example": "byot"
          },
          "access_token": {
            "type": "string",
            "description": "Write-only. Required for `byot`. The WhatsApp access token from your own Meta app. Accepted on write, never returned.\n",
            "example": "EAAG...ZDZD"
          },
          "code": {
            "type": "string",
            "description": "Required for `embedded_signup`. The short-lived authorization code from the Meta popup; naturali exchanges it for the access token server-side. Never stored or returned.\n",
            "example": "AQD...abc"
          },
          "pin": {
            "type": "string",
            "description": "Optional (`embedded_signup` only). The number's two-step-verification PIN; when present, naturali registers the number for Cloud API sending. Never stored or returned.\n",
            "example": "123456"
          },
          "waba_id": {
            "type": "string",
            "description": "Required for `embedded_signup`; optional for `byot`.",
            "example": "104444444444444"
          },
          "application_id": {
            "type": "string",
            "description": "Required for `discord`. The Discord application id.",
            "example": "1290000000000000000"
          },
          "bot_token": {
            "type": "string",
            "description": "Write-only. Required for `discord`. The bot token, sealed in naturali's encrypted store so the gateway worker can authenticate its WebSocket. Accepted on write, never returned.\n",
            "example": "MT2...bot-token"
          },
          "modes": {
            "$ref": "#/components/schemas/DiscordModes"
          }
        }
      },
      "ChannelUpdate": {
        "type": "object",
        "description": "At least one field is required. For WhatsApp, `access_token` rotates the write-only credential; for Discord, `bot_token` rotates the sealed bot token (checked with Discord as on connect) and `modes` turns the gateway flows on or off. `default` replaces the channel's default action whole when present. The worker picks either change up on its next poll and reconnects.\n",
        "properties": {
          "default": {
            "$ref": "#/components/schemas/ChannelDefaultActionInput"
          },
          "access_token": {
            "type": "string",
            "description": "WhatsApp. Write-only. Replaces the stored credential.",
            "example": "EAAG...ZDZD"
          },
          "waba_id": {
            "type": "string",
            "example": "104444444444444"
          },
          "credential_source": {
            "type": "string",
            "enum": [
              "byot",
              "embedded_signup"
            ],
            "example": "embedded_signup"
          },
          "bot_token": {
            "type": "string",
            "description": "Discord. Write-only. Rotates the sealed bot token.",
            "example": "MT2...bot-token"
          },
          "modes": {
            "$ref": "#/components/schemas/DiscordModes"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ],
            "example": "disabled"
          }
        }
      },
      "DiscordModes": {
        "type": "object",
        "nullable": true,
        "description": "Which Discord Gateway flows this channel serves; null for non-Discord channels. At least one must be enabled.\n* `dms` — the bot converses 1:1 in direct messages (the WhatsApp shape).\n  Needs only the non-privileged `DIRECT_MESSAGES` intent.\n\n* `mention_threads` — an @mention in a server opens a thread, and the\n  whole thread is one conversation. Reading the follow-up messages in\n  the thread needs the **privileged** `MESSAGE_CONTENT` intent, which\n  you enable on your own Discord app (Discord requires bot verification\n  past ~100 servers), so it is off unless you ask for it.\n",
        "properties": {
          "dms": {
            "type": "boolean",
            "default": true,
            "example": true
          },
          "mention_threads": {
            "type": "boolean",
            "default": false,
            "example": false
          }
        }
      },
      "ChannelList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Channel"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Cursor for the next page, or null when there are no more.",
            "example": null
          }
        }
      },
      "ChannelDefaultAction": {
        "type": "object",
        "description": "What happens when no route matches and the address has no action of its own (CHANNELS-ROUTING.md §3.3.1) — the same shape a `route` (`routes.yaml`) and an `address` (`addresses.yaml`) carry. For a Discord channel, `config.discord.allowed_role_ids` / `allowed_user_ids` restrict who may invoke the agent in a server — either list admits, empty or absent lists mean everyone, decided before the agent is invoked so an unlisted member costs nothing.\n",
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "agent",
              "oneshot",
              "message",
              "silence"
            ],
            "example": "agent",
            "description": "What an inbound message becomes.\n\n`agent` is a dialogue: the identity behind the message gets a session, and every later message on it continues the same one. `oneshot` runs the agent once and keeps nothing — no address, no actor, no session, no conversation — so each message is answered on its own. Choose it for work that is filed rather than discussed; choose `agent` when the answer depends on what came before.\n\n`message` delivers fixed text without running an agent, and `silence` answers nothing.\n"
          },
          "agent_id": {
            "type": "string",
            "nullable": true,
            "description": "The agent that answers, when `action` is `agent` or `oneshot`.",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "text": {
            "type": "string",
            "nullable": true,
            "description": "The fixed text delivered, when `action` is `message`.",
            "example": "Subscribe at https://example.com/pricing"
          },
          "repeat": {
            "description": "Null unless `action` is `message`. How often it fires (CHANNELS-ROUTING.md §3.3.3): `every` (default), `once`, or `{ after_seconds }`. Null unless `action` is `message`.\n",
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "every",
                  "once"
                ]
              },
              {
                "type": "object",
                "required": [
                  "after_seconds"
                ],
                "properties": {
                  "after_seconds": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              }
            ],
            "example": "every"
          },
          "language": {
            "type": "string",
            "nullable": true,
            "description": "Preferred reply language; null lets the agent decide.",
            "example": "en-US"
          },
          "config": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Conversation config bag — media handling, persona overrides, the Discord allowlist, and how a `oneshot` answers.\n\n`discord.reply` picks the delivery: `react` (the default) adds `discord.reaction` — `\\u2705` unless named — to the message that triggered the run, `mention` answers beside it addressing whoever asked, `react_or_mention` reacts when the agent writes nothing and answers like `mention` when it writes a line, `thread` opens a thread and answers inside it, and `none` delivers nothing at all. A run that fails delivers nothing in every mode, so a reaction always means the work was done; there is no failure marker.\n\nA mention with nothing else in it is ignored unless the action says otherwise, for any action. `discord.use_replied_message: true` takes the text of the message it replies to, so replying to a message with only the mention files that message. `discord.empty_mention_text` answers any other bare mention with that fixed text, addressing the sender, without running the agent.\n\n`discord.forward_user_id: true` hands the sender's Discord user id to the agent's tools as the `discord_user_id` tool context, which a tool's `headers` read as `{{context:discord_user_id}}`. It is the id Discord reported for the message's author, never text the model wrote, so a tool can act for whoever sent it. Off unless set, because tool context reaches every tool the agent has.\n\nRead only for `oneshot`. A conversational `agent` in a guild *is* its thread — that is what its session is keyed on — so it always opens one.\n",
            "example": {
              "media": {
                "audio": "transcribe"
              },
              "discord": {
                "allowed_role_ids": [
                  "1290000000000000042"
                ]
              }
            }
          }
        },
        "required": [
          "action",
          "agent_id",
          "text",
          "repeat",
          "language",
          "config"
        ]
      },
      "ChannelDefaultActionInput": {
        "type": "object",
        "description": "`action` is required; `agent_id` is required when it is `agent`, `text` when it is `message`. `repeat` is only valid alongside `action: message`.\n",
        "required": [
          "action"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "agent",
              "oneshot",
              "message",
              "silence"
            ],
            "example": "agent",
            "description": "What an inbound message becomes.\n\n`agent` is a dialogue: the identity behind the message gets a session, and every later message on it continues the same one. `oneshot` runs the agent once and keeps nothing — no address, no actor, no session, no conversation — so each message is answered on its own. Choose it for work that is filed rather than discussed; choose `agent` when the answer depends on what came before.\n\n`message` delivers fixed text without running an agent, and `silence` answers nothing.\n"
          },
          "agent_id": {
            "type": "string",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "text": {
            "type": "string",
            "example": "Subscribe at https://example.com/pricing"
          },
          "repeat": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "every",
                  "once"
                ]
              },
              {
                "type": "object",
                "required": [
                  "after_seconds"
                ],
                "properties": {
                  "after_seconds": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              }
            ],
            "example": "every"
          },
          "language": {
            "type": "string",
            "example": "en-US"
          },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "example": {
              "discord": {
                "allowed_role_ids": [
                  "1290000000000000042"
                ]
              }
            }
          }
        }
      },
      "Conversation": {
        "type": "object",
        "description": "An address's dialogue on a channel — naturali-native, keyed by (channel, address) so the same identifier never opens two conversations, and mapped 1:1 to the runtime session that carries the message history.\n",
        "required": [
          "id",
          "project_id",
          "channel_id",
          "address_id",
          "route_id",
          "identifier",
          "actor_id",
          "session_id",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "conv_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "channel_id": {
            "type": "string",
            "example": "chan_V1StGXR8Z5jdHi6B"
          },
          "address_id": {
            "type": "string",
            "description": "The address the dialogue arrived at — the continuity key.\n",
            "example": "addr_V1StGXR8Z5jdHi6B"
          },
          "route_id": {
            "type": "string",
            "description": "The route this conversation's action came from — a real route id, or one of the two sentinels for \"the address's own action\" / \"the channel default\" (CHANNELS-ROUTING.md §3.8). A message resolving to a different action opens a new conversation rather than re-pointing this one.\n",
            "example": "route_V1StGXR8Z5jdHi6B"
          },
          "identifier": {
            "type": "string",
            "nullable": true,
            "description": "The prefixed, self-describing channel identifier (`whatsapp:dm:…`, `discord:dm:…`, `discord:thread:…`). Read through from the address above.\n",
            "example": "whatsapp:dm:425678901234567890"
          },
          "actor_id": {
            "type": "string",
            "nullable": true,
            "description": "The runtime actor the address speaks as. Null when the address has not needed one yet.\n",
            "example": "actor_V1StGXR8Z5jdHi6B"
          },
          "session_id": {
            "type": "string",
            "description": "The runtime session this conversation maps 1:1 to.",
            "example": "sess_V1StGXR8Z5jdHi6B"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ConversationList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Conversation"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Cursor for the next page, or null when there are no more.",
            "example": null
          }
        }
      },
      "ConversationMessage": {
        "type": "object",
        "description": "One message in the conversation's transcript, as the runtime records it.",
        "properties": {
          "document_id": {
            "type": "string",
            "nullable": true,
            "description": "The runtime document holding the message text."
          },
          "role": {
            "type": "string",
            "nullable": true,
            "enum": [
              "user",
              "assistant",
              "system",
              null
            ],
            "example": "user"
          },
          "content": {
            "type": "string",
            "nullable": true,
            "example": "como isso difere de uma orquestração?"
          },
          "position": {
            "type": "integer",
            "nullable": true,
            "description": "Zero-based position in the conversation.",
            "example": 0
          },
          "actor_id": {
            "type": "string",
            "nullable": true,
            "description": "The actor who authored the message, when set."
          },
          "agent_id": {
            "type": "string",
            "nullable": true,
            "description": "The agent that produced the message, for assistant turns."
          },
          "metadata": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          }
        }
      },
      "ConversationMessageList": {
        "type": "object",
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConversationMessage"
            }
          },
          "total": {
            "type": "integer",
            "nullable": true,
            "description": "Total messages in the conversation, when the upstream reports it.",
            "example": 4
          },
          "limit": {
            "type": "integer",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "example": 0
          }
        }
      },
      "ConversationRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Conversation ID",
            "example": "conv_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-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-naturali-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-naturali-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-naturali-ref": "actors",
            "type": "string",
            "nullable": true,
            "description": "Optional actor ID associated with this message",
            "example": "actor_V1StGXR8Z5jdHi6B"
          },
          "agent_id": {
            "x-naturali-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/NullableMetadataBag"
              }
            ]
          },
          "content": {
            "type": "string",
            "nullable": true,
            "description": "Full text content of the message"
          }
        }
      },
      "GenerateConversationMessageCompleted": {
        "type": "object",
        "required": [
          "status",
          "content",
          "message",
          "generation_id",
          "trace_id"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "completed"
            ],
            "description": "Indicates generation finished successfully."
          },
          "content": {
            "type": "string",
            "description": "The AI-generated text of the reply. This is the canonical field for the assistant's response text.\n",
            "example": "Hello! How can I help you today?"
          },
          "message": {
            "$ref": "#/components/schemas/ConversationMessageRecord"
          },
          "generation_id": {
            "x-naturali-ref": "generations",
            "type": "string",
            "description": "ID of the underlying generation record.",
            "example": "gen_V1StGXR8Z5jdHi6B"
          },
          "trace_id": {
            "x-naturali-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-naturali-ref": "generations",
            "type": "string",
            "description": "ID of the paused generation. Pass to the tool-outputs endpoint.",
            "example": "gen_V1StGXR8Z5jdHi6B"
          },
          "trace_id": {
            "x-naturali-ref": "traces",
            "type": "string",
            "description": "Trace ID for observability.",
            "example": "trace_V1StGXR8Z5jdHi6B"
          },
          "required_action": {
            "type": "object",
            "description": "Tool-call information the client must resolve."
          }
        }
      },
      "GenerateConversationMessageResponse": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/GenerateConversationMessageCompleted"
          },
          {
            "$ref": "#/components/schemas/GenerateConversationMessageRequiresAction"
          }
        ],
        "discriminator": {
          "propertyName": "status",
          "mapping": {
            "completed": "#/components/schemas/GenerateConversationMessageCompleted",
            "requires_action": "#/components/schemas/GenerateConversationMessageRequiresAction"
          }
        }
      },
      "DeciderQuestion": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "instructions"
        ],
        "description": "One question and its finite answer space. `criteria` is an object mapping each option to its description for `choice` (2 to 20 options, in the order the model is shown them), an ordered array of level descriptions for `score` (2 to 20 levels; the answer is a level's zero-based index), and for `boolean` an optional object describing what `false` and `true` mean.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "choice",
              "score",
              "boolean"
            ],
            "example": "choice"
          },
          "instructions": {
            "type": "string",
            "description": "What to judge",
            "example": "Which team should own this ticket?"
          },
          "criteria": {
            "description": "The answer space's descriptions; the shape follows `type`",
            "oneOf": [
              {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ],
            "example": {
              "billing": "Charges, refunds, invoices, plan changes",
              "technical": "Errors, outages, integration failures"
            }
          }
        }
      },
      "DeciderQuestions": {
        "type": "object",
        "description": "Question id → question, 1 to 20 of them. A question id starts with a letter or underscore and holds only letters, digits and underscores (at most 64), since it keys the answer object.",
        "additionalProperties": {
          "$ref": "#/components/schemas/DeciderQuestion"
        },
        "example": {
          "route": {
            "type": "choice",
            "instructions": "Which team should own this ticket?",
            "criteria": {
              "billing": "Charges, refunds, invoices, plan changes",
              "technical": "Errors, outages, integration failures"
            }
          },
          "needs_human": {
            "type": "boolean",
            "instructions": "Must a person read this before any automated reply?"
          }
        }
      },
      "Decider": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "dcd_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-ref": "projects",
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "name": {
            "type": "string",
            "example": "support-triage"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "agent_id": {
            "x-naturali-ref": "agents",
            "type": "string",
            "nullable": true,
            "description": "The tool-less agent that answers; null when a tool does",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "tool_id": {
            "x-naturali-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": {
          "name": {
            "type": "string",
            "description": "Human-readable name, unique per project",
            "example": "support-triage"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "agent_id": {
            "x-naturali-ref": "agents",
            "type": "string",
            "description": "A tool-less agent that answers",
            "example": "agent_V1StGXR8Z5jdHi6B"
          },
          "tool_id": {
            "x-naturali-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-naturali-ref": "agents",
            "type": "string",
            "description": "Replaces the backend with this agent"
          },
          "tool_id": {
            "x-naturali-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-naturali-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": {
            "x-naturali-ref": "users",
            "type": "string",
            "nullable": true
          },
          "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": [
          "state"
        ],
        "properties": {
          "state": {
            "description": "What is judged: any JSON value. A string reaches the agent verbatim; anything else is serialized as JSON. Not stored.",
            "example": {
              "subject": "Charged twice",
              "body": "I was charged twice for the same order."
            }
          },
          "metadata": {
            "description": "Caller-owned annotations stored on the decision and returned on every read — typically the id of what was judged, since the state 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-naturali-tool-forced": true,
            "description": "True evaluates before answering and returns the settled decision. False — the default — returns the `queued` decision at once. An MCP tool call always waits.",
            "example": true
          }
        }
      },
      "DecisionAnswer": {
        "type": "object",
        "description": "One question's answer, confined to its answer space. `choice` carries the chosen option, `score` the level's zero-based index and its `legend`, `boolean` the `value`. `probabilities` is present only when a tool backend supplied it.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "choice",
              "score",
              "boolean"
            ]
          },
          "choice": {
            "type": "string",
            "example": "technical"
          },
          "score": {
            "type": "integer",
            "example": 2
          },
          "legend": {
            "type": "string",
            "example": "Blocks one workflow for one customer"
          },
          "value": {
            "type": "boolean",
            "example": false
          },
          "probabilities": {
            "type": "object",
            "description": "A distribution over the answer space, keyed by option, by level index as a string, or by `true` / `false`. Carried as the tool supplied it; the runtime does not vouch for its meaning.",
            "additionalProperties": {
              "type": "number"
            },
            "example": {
              "billing": 0.8,
              "technical": 0.2
            }
          }
        }
      },
      "Decision": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "dec_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-ref": "projects",
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "decider_id": {
            "x-naturali-ref": "deciders",
            "type": "string",
            "description": "The decider asked; kept after the decider is deleted",
            "example": "dcd_V1StGXR8Z5jdHi6B"
          },
          "decider_version": {
            "type": "integer",
            "description": "The question-set version the decision was answered under",
            "example": 1
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "completed",
              "failed"
            ]
          },
          "answers": {
            "type": "object",
            "nullable": true,
            "description": "Question id → answer. Null until the decision completes, then never rewritten.",
            "additionalProperties": {
              "$ref": "#/components/schemas/DecisionAnswer"
            }
          },
          "error": {
            "type": "object",
            "nullable": true,
            "description": "Why a `failed` decision failed.",
            "properties": {
              "code": {
                "type": "string",
                "example": "OUTPUT_SCHEMA_VALIDATION_FAILED"
              },
              "message": {
                "type": "string"
              }
            }
          },
          "generation_id": {
            "x-naturali-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/NullableMetadataBag"
              }
            ]
          },
          "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
        }
      },
      "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-naturali-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 the runtime 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": {
            "type": "string",
            "nullable": true,
            "description": "Public ID of the user whose write produced this version; null for a write with no request user behind it.",
            "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-naturali-ref": "files",
            "type": "string",
            "description": "Underlying file ID",
            "example": "file_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-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
          },
          "relations": {
            "type": "array",
            "description": "The typed edges this document asserts about others, on a single document read. Absent from a listing, which is a page of documents rather than of edges.",
            "items": {
              "$ref": "#/components/schemas/DocumentRelation"
            }
          },
          "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": {
            "type": "string",
            "nullable": true,
            "description": "Failure reason when `status` is `failed` (e.g. `FILE_PARSE_FAILED`, `INGESTION_TIMEOUT`).",
            "example": "INGESTION_TIMEOUT"
          }
        }
      },
      "EmbeddingsResponse": {
        "type": "object",
        "description": "Response containing generated embeddings. Fields present depend on whether `input` or `inputs` was provided.",
        "properties": {
          "embedding": {
            "type": "array",
            "description": "Embedding vector for the single `input` text.",
            "items": {
              "type": "number"
            },
            "example": [
              0.123,
              -0.456,
              0.789
            ]
          },
          "embeddings": {
            "type": "array",
            "description": "Embedding vectors for each item in the `inputs` batch.",
            "items": {
              "type": "array",
              "items": {
                "type": "number"
              }
            },
            "example": [
              [
                0.123,
                -0.456,
                0.789
              ],
              [
                0.321,
                -0.654,
                0.987
              ]
            ]
          }
        }
      },
      "DatasetItemInput": {
        "type": "array",
        "description": "Messages replayed verbatim as the generation's input",
        "items": {
          "type": "object",
          "required": [
            "role",
            "content"
          ],
          "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-naturali-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`, 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 `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-naturali-ref": "deciders",
            "type": "string",
            "description": "The decider that judges each item; it must belong to the eval's project.",
            "example": "dcd_V1StGXR8Z5jdHi6B"
          },
          "state": {
            "description": "JSON Logic mapping over `{ input, output, object, expected, item }` building the state the decider judges. Omitted, the state is that item context itself.",
            "example": {
              "customer": {
                "var": "input.0.content"
              },
              "reply": {
                "var": "output"
              }
            }
          },
          "score": {
            "description": "JSON Logic expression over `{ answers }`, the decision's answers keyed by question id, that evaluates to the item's score in 0–1.",
            "example": {
              "var": "answers.resolves_issue.probabilities.true"
            }
          },
          "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-naturali-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/NullableMetadataBag"
          },
          "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-naturali-ref": "projects",
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "agent_id": {
            "x-naturali-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",
              "canceled"
            ]
          },
          "baseline_run_id": {
            "type": "string",
            "nullable": true,
            "example": "evrun_V1StGXR8Z5jdHi6B"
          },
          "trigger_id": {
            "type": "string",
            "nullable": true,
            "description": "The trigger that started this run — set when a schedule (or a manual trigger fire) started it, null for a run started through this API. Kept even if the trigger is later deleted.",
            "example": "trg_V1StGXR8Z5jdHi6B"
          },
          "aggregate_scores": {
            "$ref": "#/components/schemas/AggregateScores"
          },
          "passed": {
            "type": "boolean",
            "nullable": true,
            "description": "Null when the eval declares no pass_threshold, and until the run is terminal"
          },
          "item_count": {
            "type": "integer"
          },
          "completed_count": {
            "type": "integer"
          },
          "errored_count": {
            "type": "integer"
          },
          "metadata": {
            "description": "The caller-owned key/value metadata supplied when the run was started, returned verbatim. Null when the run was started without any (a trigger-started run included — see `trigger_id` for that provenance). The server writes nothing here.",
            "example": {
              "commit_sha": "9f2c1ab",
              "ci_job": "nightly-evals"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/NullableMetadataBag"
              }
            ]
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "finished_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EvalResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "evres_V1StGXR8Z5jdHi6B"
          },
          "eval_run_id": {
            "type": "string",
            "example": "evrun_V1StGXR8Z5jdHi6B"
          },
          "dataset_item_id": {
            "type": "string",
            "nullable": true,
            "description": "Null once the dataset item has been deleted",
            "example": "dsit_V1StGXR8Z5jdHi6B"
          },
          "input": {
            "$ref": "#/components/schemas/DatasetItemInput"
          },
          "expected_output": {
            "type": "string",
            "nullable": true
          },
          "generation_id": {
            "type": "string",
            "nullable": true,
            "example": "gen_V1StGXR8Z5jdHi6B"
          },
          "output": {
            "type": "string",
            "nullable": true,
            "description": "The agent's final output text. Cleared when the linked generation's content is purged; the scores and the frozen input survive."
          },
          "scores": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScorerResult"
            }
          },
          "passed": {
            "type": "boolean",
            "description": "AND over the per-scorer passed flags"
          },
          "error": {
            "type": "string",
            "nullable": true,
            "description": "Item-level failure reason. A generation that did not complete — a `requires_action` pause, a provider failure — is recorded here and excluded from the aggregates rather than scored 0."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ExceptionItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "exc_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-ref": "projects",
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "acknowledged",
              "resolved"
            ]
          },
          "severity": {
            "type": "string",
            "enum": [
              "info",
              "warning",
              "critical"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "run_failed",
              "guardrail_tripwire",
              "approval_expired",
              "quota_unpriced",
              "event_trigger_loop",
              "chain_limit",
              "manual"
            ],
            "description": "How the exception was filed"
          },
          "title": {
            "type": "string",
            "description": "Human-readable one-line summary"
          },
          "detail": {
            "type": "object",
            "nullable": true,
            "description": "Structured context (tool, args digest, error message, guardrail version)"
          },
          "occurrence_count": {
            "type": "integer",
            "description": "How many times this exact failure has been observed while open"
          },
          "last_seen_at": {
            "type": "string",
            "format": "date-time",
            "description": "Timestamp of the most recent occurrence"
          },
          "orchestration_run_id": {
            "type": "string",
            "nullable": true,
            "description": "Originating orchestration run"
          },
          "node_id": {
            "type": "string",
            "nullable": true,
            "description": "Originating node id within the run's graph"
          },
          "agent_id": {
            "type": "string",
            "nullable": true,
            "description": "Associated agent"
          },
          "guardrail_version": {
            "type": "string",
            "nullable": true,
            "description": "`<guardrailId>@<version>` for a guardrail_tripwire exception"
          },
          "acknowledged_by": {
            "type": "string",
            "nullable": true,
            "description": "Acknowledging user's public ID"
          },
          "resolved_by": {
            "type": "string",
            "nullable": true,
            "description": "Resolving user's public ID"
          },
          "resolution_note": {
            "type": "string",
            "nullable": true,
            "description": "Optional note recorded at resolution"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "UploadFileBase64Request": {
        "type": "object",
        "required": [
          "content"
        ],
        "additionalProperties": false,
        "properties": {
          "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"
          }
        }
      },
      "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/NullableMetadataBag"
          },
          "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"
          }
        }
      },
      "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}"
      },
      "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-naturali-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-naturali-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-naturali-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-naturali-ref": "tools",
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "Subset of the bound tools that are active"
          },
          "guardrail_ids": {
            "x-naturali-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-naturali-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 runtime 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."
          },
          "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-naturali-ref": "memory-stores",
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Public IDs of memory stores to retrieve from"
              },
              "document_ids": {
                "x-naturali-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-naturali-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 turn's static prefix — the tool definitions and the instructions together. 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-naturali-ref": "agents",
            "type": "string",
            "nullable": true,
            "description": "Linked agent ID (mutually exclusive with chat_id)"
          },
          "chat_id": {
            "x-naturali-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-naturali-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, the runtime 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, 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`."
              },
              "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 `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-naturali-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-naturali-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-naturali-ref": "agents",
            "type": "string",
            "description": "Public ID of the agent under test (or ref expression)"
          },
          "dataset_id": {
            "x-naturali-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-naturali-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-naturali-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)"
          }
        }
      },
      "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 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-naturali-ref": "policies",
            "type": "string",
            "nullable": true,
            "description": "Optional boundary policy that further confines the run-as identity"
          }
        }
      },
      "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-naturali-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"
              }
            ]
          }
        }
      },
      "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"
          }
        }
      },
      "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-naturali-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-naturali-ref": "actors",
            "type": "string",
            "nullable": true,
            "description": "Public ID of an actor to associate with this session"
          },
          "auto_generate": {
            "type": "boolean",
            "description": "Whether to automatically generate a response when messages are sent"
          },
          "inactivity_ttl_seconds": {
            "type": "integer",
            "description": "Number of seconds of inactivity after which the session expires. 0 means never expires."
          },
          "tool_context": {
            "type": "object",
            "additionalProperties": true,
            "nullable": true,
            "description": "Optional context object passed to tool calls. 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-naturali-ref": "tools",
            "type": "string",
            "nullable": true,
            "description": "Converter tool ID (mutually exclusive with agent_id)"
          },
          "agent_id": {
            "x-naturali-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 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-naturali-ref": "memory-stores",
            "type": "string",
            "description": "Public ID of the destination memory store"
          },
          "on": {
            "type": "string",
            "description": "`agents.generation.completed` (once per completed turn, and the only event the built-in extractor may bind to) or `conversations.message.generated` (per persisted assistant reply, custom handlers only)."
          },
          "source_agent_ids": {
            "x-naturali-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-naturali-ref": "agents",
            "type": "string",
            "nullable": true,
            "description": "Handler agent ID (mutually exclusive with tool_id)"
          },
          "tool_id": {
            "x-naturali-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-naturali-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 succeeded 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-naturali-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 state. 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-naturali-ref": "agents",
            "type": "string",
            "description": "The tool-less agent that answers"
          },
          "tool_id": {
            "x-naturali-ref": "tools",
            "type": "string",
            "description": "The http or pipeline tool that answers"
          },
          "questions": {
            "type": "object",
            "additionalProperties": true,
            "description": "Question id → question (`type`, `instructions`, `criteria`), 1 to 20 of them, validated as the deciders REST contract validates them."
          }
        }
      },
      "ResourceDeclaration": {
        "type": "object",
        "required": [
          "type",
          "properties"
        ],
        "properties": {
          "type": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*$",
            "description": "Resource type. The types this API accepts are `ai_provider`, `tool`, `agent`, `actor`, `conversation`, `dataset`, `dataset_item`, `decider`, `document`, `file`, `guardrail`, `ingestion_rule`, `memory_store`, `memory`, `memory_rule`, `metadata_schema`, `model_route`, `eval`, `orchestration`, `quota`, `secret`, `session`, `trigger` and `workflow` — plus two naturali registers itself and handles through its own lifecycle: `channel`, taking the same property names the channels API takes (its credential properties are write-only, so a tenant credential never lands in the resource ledger), and `naturali_ai_provider`, which takes a `default_model` from the model catalog and provisions a provider running on naturali's own model access (it carries no credential of yours, so it accepts none). A template naming any other type is refused with `400 unsupported_resource_type` before it reaches the runtime — including the runtime's own `api_key`, `chat`, `policy`, `project_price` and `webhook` types, which this API does not expose.\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 the runtime 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-naturali-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/NullableMetadataBag"
              }
            ]
          },
          "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/FormationError"
              }
            ],
            "nullable": true,
            "description": "Why the formation is `failed` or `delete_failed`, in the same `{ code, message, meta }` shape as an error response. Null in every other status, and cleared by the next successful deploy. This is the reason a `2xx` deploy response can report `status: \"failed\"` without a second call to `list-formation-events`.\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"
          }
        }
      },
      "FormationError": {
        "type": "object",
        "description": "Why a deploy or teardown failed, in the one error shape the API has. Carried on the formation itself and on the operation that failed.",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "The failing operation's error code (`VALIDATION_FAILED`, `RESOURCE_NOT_FOUND`, `FORMATION_DELETE_FAILED`, `FORMATION_REPLACE_CLEANUP_FAILED`, …), or `UNKNOWN` when the underlying failure carried no code.",
            "example": "VALIDATION_FAILED"
          },
          "message": {
            "type": "string",
            "description": "The failure, as reported by the resource that raised it.",
            "example": "dataset_id is immutable: item 'dsit_V1StGXR8Z5jdHi6B' belongs to 'dset_V1StGXR8Z5jdHi6B'. Declare a new dataset_item instead."
          },
          "meta": {
            "type": "object",
            "additionalProperties": true,
            "description": "Context for the failure. A failed apply names the resource that broke it (`logical_id`, `resource_type`); a failed teardown lists every blocker under `failures`, and so does a succeeded deploy that could not dispose of a replaced resource — there each entry adds the `physical_resource_id` still live.",
            "example": {
              "logical_id": "case1",
              "resource_type": "dataset_item"
            }
          }
        }
      },
      "FormationEvent": {
        "type": "object",
        "properties": {
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "logical_id": {
            "type": "string"
          },
          "resource_type": {
            "type": "string"
          },
          "action": {
            "type": "string",
            "description": "What the deploy did to the resource: `create`, `update`, `delete`, `no-op`, `rollback` (a resource created earlier in this deploy that was walked back after a later failure), or `rollback-skipped` (a `deletion_policy: retain` resource left standing by that unwind).",
            "example": "rollback"
          },
          "status": {
            "type": "string",
            "enum": [
              "succeeded",
              "failed"
            ]
          },
          "physical_resource_id": {
            "type": "string",
            "nullable": true
          },
          "error": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "FormationOperation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public ID of the operation"
          },
          "operation_type": {
            "type": "string",
            "enum": [
              "validate",
              "plan",
              "create",
              "update",
              "delete"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "running",
              "succeeded",
              "failed"
            ]
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FormationEvent"
            },
            "nullable": true
          },
          "plan": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanResult"
              }
            ],
            "nullable": true
          },
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FormationError"
              }
            ],
            "nullable": true,
            "description": "Why this operation failed. Null for a succeeded or running operation. The same bag the formation itself carries while that failure is its current state."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NullableTagBag": {
        "type": "object",
        "nullable": true,
        "additionalProperties": {
          "type": "string"
        },
        "description": "A `TagBag` on a field where `null` is meaningful — a full-replacement update that clears the bag, or a record whose bag was never set.",
        "example": {
          "team": "finance",
          "env": "prod"
        }
      },
      "Generation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public ID of the generation",
            "example": "gen_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-ref": "projects",
            "type": "string",
            "description": "Public ID of the project"
          },
          "agent_id": {
            "x-naturali-ref": "agents",
            "type": "string",
            "description": "Public ID of the agent that ran this generation"
          },
          "trace_id": {
            "x-naturali-ref": "traces",
            "type": "string",
            "description": "Public ID of the trace this generation belongs to"
          },
          "initiator_generation_id": {
            "x-naturali-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-naturali-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-naturali-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-naturali-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-naturali-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.\n"
          },
          "started_by_principal_type": {
            "type": "string",
            "nullable": true,
            "description": "Type of the principal that started the generation"
          },
          "started_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "ID of the principal that started the generation"
          },
          "status": {
            "type": "string",
            "description": "Lifecycle status of the generation",
            "enum": [
              "in_progress",
              "requires_action",
              "completed",
              "failed"
            ],
            "example": "failed"
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the generation reached a terminal state"
          },
          "last_activity_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "stop_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why the generation stopped. Either the model provider's own finish reason relayed unchanged ('stop', 'tool-calls', 'length', …) or one the platform names itself: 'max_steps' when the turn spent its whole step budget on tool calls, 'depth_guard' when a nested call exceeded the call depth, 'chain_limit' when a continuation chain reached its generation budget, or 'error' when the turn failed.\n",
            "example": "error"
          },
          "error": {
            "type": "object",
            "nullable": true,
            "description": "Structured error payload recorded when the generation failed. Contains at least `message`; `code` is set for mapped errors (e.g. AI_PROVIDER_ERROR for upstream provider failures).\n",
            "properties": {
              "code": {
                "type": "string",
                "example": "AI_PROVIDER_ERROR"
              },
              "message": {
                "type": "string",
                "example": "Provider returned 402: insufficient credits"
              },
              "meta": {
                "type": "object"
              }
            }
          },
          "action_id": {
            "type": "string",
            "nullable": true,
            "description": "Logical action label supplied on the generate request. Recorded on the generation's usage event for per-action spend rollups.\n"
          },
          "trigger_id": {
            "x-naturali-ref": "triggers",
            "type": "string",
            "nullable": true,
            "description": "Trigger that initiated the generation, when applicable"
          },
          "orchestration_run_id": {
            "x-naturali-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 `agents.generation.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 `conversations.message.generated` are recorded in `memory_assertions` only. The rows behind every count are in `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)"
                }
              }
            }
          },
          "memory_assertions": {
            "type": "array",
            "description": "Every memory write this turn made, oldest first — each memory rule's firings and the agent's own `write_memory` calls alike, which the `extraction` counts never covered. Present on the single read only; a listing would make it one extra query per generation.\n",
            "items": {
              "$ref": "#/components/schemas/MemoryAssertion"
            }
          },
          "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-naturali-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-naturali-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/NullableMetadataBag"
              }
            ]
          },
          "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": {
            "type": "string",
            "nullable": true,
            "description": "Principal kind that purged the content ('user' or 'api_key')",
            "example": "user"
          },
          "content_redacted_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public ID of the principal that purged the content — the API key's own id for key auth, so the record names which key acted.\n"
          },
          "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-naturali-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-naturali-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 the runtime.\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": {
            "description": "The tool's failure, when the step recorded one.",
            "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-naturali-ref": "generations",
            "type": "string",
            "example": "gen_V1StGXR8Z5jdHi6B"
          },
          "trace_id": {
            "x-naturali-ref": "traces",
            "type": "string",
            "nullable": true,
            "example": "trace_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-ref": "projects",
            "type": "string"
          },
          "agent_id": {
            "x-naturali-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": [
              "in_progress",
              "requires_action",
              "completed",
              "failed"
            ],
            "example": "completed"
          },
          "stop_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why the generation stopped — the provider's finish reason, or one of the platform's own ('max_steps', 'depth_guard', 'chain_limit', 'error').\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": {
            "type": "object",
            "nullable": true,
            "description": "Structured error payload when the generation failed."
          },
          "content_redacted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the generation's content was erased; null while it is intact.\n"
          },
          "content_redacted_by_principal_type": {
            "type": "string",
            "nullable": true,
            "description": "Principal kind that erased the content.",
            "example": "system"
          },
          "content_redacted_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public ID of that principal. `zero_retention` when the content was never stored, distinguishing it from content erased later.\n"
          }
        }
      },
      "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-naturali-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 /v1/projects/{project_id}/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-naturali-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-naturali-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": {
            "type": "string",
            "description": "Who claimed the fact, in the vocabulary a generation records its starter with, plus `agent`. On both agent doors the principal is the agent itself — the extractor runs under its identity.",
            "example": "agent"
          },
          "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"
          }
        }
      },
      "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."
          }
        }
      },
      "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-naturali-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-naturali-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-naturali-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-naturali-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 the runtime reflects the guardrail surface **of its own time**, so it may carry fields the current API no longer documents.",
            "properties": {
              "document": {
                "$ref": "#/components/schemas/GuardrailDocument"
              }
            }
          },
          "label": {
            "type": "string",
            "nullable": true,
            "description": "Optional human tag for this version, e.g. `pre-tightening`. Set from the `label` field of a restore, or generated for one.",
            "example": "restored from v2"
          },
          "created_by": {
            "x-naturali-ref": "users",
            "type": "string",
            "nullable": true,
            "description": "Public ID of the user whose action produced this version. Null for writes with no request user behind them."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RestoreGuardrailVersionRequest": {
        "type": "object",
        "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": {
          "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-naturali-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-naturali-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-naturali-ref": "projects",
            "type": "string"
          },
          "content_type_glob": {
            "type": "string",
            "example": "image/*"
          },
          "tool_id": {
            "x-naturali-ref": "tools",
            "type": "string",
            "nullable": true
          },
          "agent_id": {
            "x-naturali-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/NullableMetadataBag"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Install": {
        "type": "object",
        "description": "A listing installed in a project.",
        "properties": {
          "id": {
            "type": "string",
            "example": "ins_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "description": "The installing project.",
            "example": "proj_6NgFz3xXy2kPq9Wd"
          },
          "listing_id": {
            "type": "string",
            "example": "lst_V1StGXR8Z5jdHi6B"
          },
          "resource_type": {
            "type": "string",
            "enum": [
              "tool",
              "agent"
            ],
            "example": "tool"
          },
          "resource_id": {
            "type": "string",
            "description": "The id the project names the installed resource by.",
            "example": "tool_V1StGXR8Z5jdHi6B"
          },
          "state": {
            "type": "string",
            "enum": [
              "active",
              "suspended",
              "blocked"
            ],
            "description": "`active` while usable, `suspended` while its listing is, `blocked` after the publisher removed the project's access.\n",
            "example": "active"
          },
          "installed_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-01T00:00:00.000Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-01T00:00:00.000Z"
          }
        },
        "required": [
          "id",
          "project_id",
          "listing_id",
          "resource_type",
          "resource_id",
          "state",
          "installed_at",
          "updated_at"
        ]
      },
      "InstallList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Install"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Cursor for the next page, or null when there are no more.",
            "example": null
          }
        }
      },
      "InstallCreate": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "listing_id"
        ],
        "properties": {
          "listing_id": {
            "type": "string",
            "minLength": 1,
            "example": "lst_V1StGXR8Z5jdHi6B"
          }
        }
      },
      "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-naturali-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-naturali-ref": "files",
            "type": "string",
            "description": "Public ID of the underlying file",
            "example": "file_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-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-naturali-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"
          },
          "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"
          }
        }
      },
      "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"
            ]
          }
        }
      },
      "ListingResourceType": {
        "type": "string",
        "enum": [
          "tool",
          "agent"
        ],
        "description": "What the listing publishes.",
        "example": "tool"
      },
      "ListingState": {
        "type": "string",
        "enum": [
          "draft",
          "in_review",
          "listed",
          "suspended"
        ],
        "description": "`draft` until submitted, `in_review` until naturali decides, `listed` in the marketplace, `suspended` when paused by its publisher or by naturali.\n",
        "example": "listed"
      },
      "Listing": {
        "type": "object",
        "description": "A listing, as its publisher reads it.",
        "properties": {
          "id": {
            "type": "string",
            "example": "lst_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "description": "The publishing project.",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "resource_type": {
            "$ref": "#/components/schemas/ListingResourceType"
          },
          "resource_id": {
            "type": "string",
            "description": "The published tool or agent.",
            "example": "tool_V1StGXR8Z5jdHi6B"
          },
          "title": {
            "type": "string",
            "example": "Invoice OCR"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Reads a scanned invoice and returns its fields."
          },
          "pricing": {
            "type": "array",
            "description": "What an installing project is charged per call. Empty is free.",
            "items": {
              "$ref": "#/components/schemas/ListingPricingComponent"
            }
          },
          "pricing_next": {
            "$ref": "#/components/schemas/ListingPricingNext"
          },
          "state": {
            "$ref": "#/components/schemas/ListingState"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-01T00:00:00.000Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-01T00:00:00.000Z"
          }
        },
        "required": [
          "id",
          "project_id",
          "resource_type",
          "resource_id",
          "title",
          "description",
          "pricing",
          "pricing_next",
          "state",
          "created_at",
          "updated_at"
        ]
      },
      "ListingPricingComponent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "component",
          "unit",
          "quantity",
          "unit_price"
        ],
        "properties": {
          "component": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]{0,39}$",
            "description": "The publisher's name for what is charged. Must not be a name the runtime already meters, such as `input_tokens` or `tool_call`.\n",
            "example": "page"
          },
          "unit": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "example": "count"
          },
          "quantity": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "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. `null` charges one per call. A failed call is never charged a component.\n",
            "example": {
              "var": "response.page_count"
            }
          },
          "unit_price": {
            "type": "number",
            "minimum": 0,
            "description": "US dollars per `unit`.",
            "example": 0.002
          }
        }
      },
      "ListingPricingNext": {
        "type": "object",
        "nullable": true,
        "required": [
          "effective_from",
          "pricing"
        ],
        "description": "A price increase waiting out its notice. Installing projects are charged `pricing` until `effective_from`, then this.\n",
        "properties": {
          "effective_from": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-08T00:01:00.000Z"
          },
          "pricing": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListingPricingComponent"
            }
          }
        }
      },
      "ListingList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Listing"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Cursor for the next page, or null when there are no more.",
            "example": null
          }
        }
      },
      "ListingCreate": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "resource_type",
          "resource_id",
          "title"
        ],
        "properties": {
          "resource_type": {
            "$ref": "#/components/schemas/ListingResourceType"
          },
          "resource_id": {
            "type": "string",
            "minLength": 1,
            "description": "A tool or agent in this project.",
            "example": "tool_V1StGXR8Z5jdHi6B"
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "example": "Invoice OCR"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "maxLength": 2000,
            "example": "Reads a scanned invoice and returns its fields."
          },
          "pricing": {
            "type": "array",
            "maxItems": 10,
            "description": "What an installing project is charged per call, on top of any model cost. Omit for a free listing.\n",
            "items": {
              "$ref": "#/components/schemas/ListingPricingComponent"
            }
          }
        }
      },
      "ListingUpdate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "example": "Invoice OCR"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "maxLength": 2000,
            "example": "Reads a scanned invoice and returns its fields."
          },
          "state": {
            "type": "string",
            "enum": [
              "suspended",
              "listed"
            ],
            "description": "Pause the listing, or resume one you paused.",
            "example": "suspended"
          },
          "pricing": {
            "type": "array",
            "maxItems": 10,
            "description": "The new price, replacing the whole list. A lower price applies the next minute. A higher unit price or a new component applies 7 days later, is returned in `pricing_next` meanwhile, and is emailed to every installing project. A component left out is no longer charged.\n",
            "items": {
              "$ref": "#/components/schemas/ListingPricingComponent"
            }
          }
        }
      },
      "PublicListing": {
        "type": "object",
        "description": "A listed entry, as any project reads it.",
        "properties": {
          "id": {
            "type": "string",
            "example": "lst_V1StGXR8Z5jdHi6B"
          },
          "resource_type": {
            "$ref": "#/components/schemas/ListingResourceType"
          },
          "resource_id": {
            "type": "string",
            "description": "The id an installing project names the resource by.",
            "example": "tool_V1StGXR8Z5jdHi6B"
          },
          "title": {
            "type": "string",
            "example": "Invoice OCR"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Reads a scanned invoice and returns its fields."
          },
          "pricing": {
            "type": "array",
            "description": "What an installing project is charged per call. Empty is free.",
            "items": {
              "$ref": "#/components/schemas/ListingPricingComponent"
            }
          },
          "pricing_next": {
            "$ref": "#/components/schemas/ListingPricingNext"
          },
          "interface": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "What an installing project sees of the resource: `id`, `name`, `description` and `parameters` for a tool, `id` and `name` for an agent. `null` when it cannot be read right now.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-01T00:00:00.000Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-01T00:00:00.000Z"
          }
        },
        "required": [
          "id",
          "resource_type",
          "resource_id",
          "title",
          "description",
          "pricing",
          "pricing_next",
          "interface",
          "created_at",
          "updated_at"
        ]
      },
      "PublicListingList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicListing"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Cursor for the next page, or null when there are no more.",
            "example": null
          }
        }
      },
      "ListingInstall": {
        "type": "object",
        "description": "One project's install, as the publisher reads it.",
        "properties": {
          "id": {
            "type": "string",
            "example": "ins_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "description": "The installing project.",
            "example": "proj_6NgFz3xXy2kPq9Wd"
          },
          "project_name": {
            "type": "string",
            "nullable": true,
            "example": "Acme support"
          },
          "state": {
            "type": "string",
            "enum": [
              "active",
              "suspended",
              "blocked"
            ],
            "example": "active"
          },
          "installed_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-01T00:00:00.000Z"
          },
          "calls_cycle": {
            "type": "integer",
            "nullable": true,
            "description": "Calls through the listing this billing cycle; null before the first sample.",
            "example": 1280
          },
          "errors_cycle": {
            "type": "integer",
            "nullable": true,
            "description": "Failed tool calls this billing cycle; null for an agent or before the first sample.",
            "example": 3
          },
          "sampled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the two counts were last sampled.",
            "example": "2026-10-01T00:15:00.000Z"
          }
        },
        "required": [
          "id",
          "project_id",
          "project_name",
          "state",
          "installed_at",
          "calls_cycle",
          "errors_cycle",
          "sampled_at"
        ]
      },
      "ListingInstallList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListingInstall"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Cursor for the next page, or null when there are no more.",
            "example": null
          }
        }
      },
      "Memory": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "mem_V1StGXR8Z5jdHi6B"
          },
          "memory_store_id": {
            "x-naturali-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/NullableTagBag"
              }
            ]
          },
          "metadata": {
            "description": "Arbitrary structured metadata attached to the memory",
            "example": {
              "evidence": "high"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/NullableMetadataBag"
              }
            ]
          },
          "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 /v1/projects/{project_id}/memories/{memory_id}/assertions`."
              }
            }
          }
        ]
      },
      "MemoryRuleEvent": {
        "type": "string",
        "enum": [
          "agents.generation.completed",
          "conversations.message.generated"
        ],
        "description": "The event a firing reads. `agents.generation.completed` fires once per completed turn whatever the transport, and is the only event the built-in extractor may bind to. `conversations.message.generated` fires per persisted assistant reply and is conversation-backed, so it is for custom handlers.",
        "example": "agents.generation.completed"
      },
      "MemoryRule": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "mrule_V1StGXR8Z5jdHi6B"
          },
          "memory_store_id": {
            "x-naturali-ref": "memory-stores",
            "type": "string"
          },
          "project_id": {
            "x-naturali-ref": "projects",
            "type": "string"
          },
          "on": {
            "$ref": "#/components/schemas/MemoryRuleEvent"
          },
          "source_agent_ids": {
            "type": "array",
            "nullable": true,
            "items": {
              "x-naturali-ref": "agents",
              "type": "string"
            }
          },
          "agent_id": {
            "x-naturali-ref": "agents",
            "type": "string",
            "nullable": true
          },
          "tool_id": {
            "x-naturali-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-naturali-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-naturali-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/NullableTagBag"
              }
            ]
          },
          "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"
          }
        }
      },
      "MetadataSchemaRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public ID (mdschema_ prefix)",
            "example": "mdschema_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-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-naturali-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-naturali-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"
          }
        }
      },
      "Model": {
        "type": "object",
        "properties": {
          "model": {
            "type": "string",
            "description": "The model's name, and the only model string this API accepts or returns. Use it verbatim as a provider's `default_model` and as an agent's `model`.\n\nIt is naturali's own name, not the vendor's: it states a version explicitly and never changes, so a vendor relabelling its model cannot move a name you have already written down. A vendor's own invocation string is refused where this is expected — one model has one name.\n",
            "example": "nova-lite-v1"
          },
          "vendor": {
            "type": "string",
            "description": "The model maker.",
            "example": "amazon"
          },
          "input_modalities": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "text",
              "image",
              "video"
            ],
            "description": "Accepted input modalities."
          },
          "output_modalities": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "text"
            ]
          },
          "streaming": {
            "type": "boolean",
            "example": true
          },
          "status": {
            "type": "string",
            "enum": [
              "available",
              "deprecated"
            ],
            "description": "Lifecycle status, from the provider's own listing at the last sync. A model the provider stops listing — or reports as legacy or deprecated — flips to `deprecated` and stays in the catalog, so a project already generating on it can see what happened to it. A `deprecated` model cannot be chosen as the `default_model` of a new `provider: \"naturali\"` provider.\n",
            "example": "available"
          },
          "pricing": {
            "type": "object",
            "description": "naturali's price for generating on this model, in USD per 1K tokens. Always present: a model naturali cannot price is not in this catalog. Refreshed by the daily sync.\n",
            "properties": {
              "currency": {
                "type": "string",
                "enum": [
                  "usd"
                ],
                "example": "usd"
              },
              "input_per_1k_tokens": {
                "type": "number",
                "description": "USD per 1K input (uncached) tokens.",
                "example": 0.00006
              },
              "output_per_1k_tokens": {
                "type": "number",
                "description": "USD per 1K output tokens. Reasoning (\"thinking\") tokens are output tokens: on a model that reasons by default they are counted in `output_tokens` and billed at this rate, and can be most of the output on a short answer. A rate alone therefore does not size a request on such a model.\n",
                "example": 0.00024
              },
              "cached_input_per_1k_tokens": {
                "type": "number",
                "nullable": true,
                "description": "USD per 1K cache-read input tokens; null when the model does not price caching (cached tokens are then billed at the input rate).\n",
                "example": 0.000015
              }
            },
            "required": [
              "currency",
              "input_per_1k_tokens",
              "output_per_1k_tokens",
              "cached_input_per_1k_tokens"
            ]
          },
          "synced_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the catalog last confirmed this entry against the provider.",
            "example": "2026-08-18T06:00:00.000Z"
          }
        },
        "required": [
          "model",
          "vendor",
          "input_modalities",
          "output_modalities",
          "streaming",
          "status",
          "pricing",
          "synced_at"
        ]
      },
      "ModelList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Model"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Cursor for the next page, or null at the end.",
            "example": null
          }
        }
      },
      "QueueStats": {
        "type": "object",
        "description": "A point-in-time snapshot of the orchestration run queue.",
        "properties": {
          "driver": {
            "type": "string",
            "enum": [
              "postgres",
              "sqs"
            ],
            "description": "The active queue driver (`ORCHESTRATION_QUEUE_DRIVER`). Under `sqs`, `oldest_queued_age_seconds` is always `null` and `per_project` is always empty — SQS exposes neither.",
            "example": "postgres"
          },
          "queue_depth": {
            "type": "integer",
            "description": "Tasks waiting to be claimed now (unclaimed and past their `available_at`). Backoff-delayed tasks are excluded. 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
              }
            }
          },
          "per_project": {
            "type": "array",
            "description": "One row per project with any queued or claimed task.",
            "items": {
              "type": "object",
              "properties": {
                "project_id": {
                  "type": "string",
                  "description": "Public project ID (proj_ prefix).",
                  "example": "proj_V1StGXR8Z5jdHi6B"
                },
                "queued": {
                  "type": "integer",
                  "example": 5
                },
                "claimed": {
                  "type": "integer",
                  "example": 1
                }
              }
            }
          }
        }
      },
      "OrchestrationNode": {
        "type": "object",
        "description": "A single execution unit in the orchestration graph.",
        "required": [
          "id",
          "type"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique node identifier within this orchestration."
          },
          "type": {
            "type": "string",
            "description": "Node execution type. Known types: agent, tool, transform, knowledge, 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-naturali-ref": "agents",
            "type": "string",
            "description": "For agent nodes — public ID of the agent to invoke."
          },
          "tool_id": {
            "x-naturali-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 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-naturali-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-naturali-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 succeeded 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": {
          "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-naturali-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 the runtime 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": {
            "x-naturali-ref": "users",
            "type": "string",
            "nullable": true,
            "description": "Public ID of the user whose action produced this version. Null for writes with no request user behind them."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RestoreOrchestrationVersionRequest": {
        "type": "object",
        "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-naturali-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 /v1/projects/{project_id}/orchestrations/{orchestration_id}/versions/{version}`.\n\nNull for runs created before pinning existed, which execute the live graph.\n",
            "example": 3
          },
          "project_id": {
            "x-naturali-ref": "projects",
            "type": "string",
            "description": "Public ID of the owning project."
          },
          "status": {
            "type": "string",
            "description": "Run lifecycle state. `queued` awaits a worker; `running` is actively executing; `sleeping` is parked on a delay/poll wait (no worker); `awaiting_input` is parked on a human node; `succeeded`/`failed`/ `cancelled` are terminal; `expired` is a wait that passed its deadline.",
            "enum": [
              "queued",
              "running",
              "sleeping",
              "awaiting_input",
              "succeeded",
              "failed",
              "cancelled",
              "expired"
            ]
          },
          "state": {
            "type": "object",
            "description": "Current accumulated state."
          },
          "active_nodes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Node IDs currently active."
          },
          "artifacts": {
            "type": "object",
            "description": "Map of node ID to output artifact."
          },
          "error": {
            "type": "object",
            "nullable": true,
            "description": "Error details when status is failed."
          },
          "trace_id": {
            "x-naturali-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. Null when the run was started without any. The server writes nothing here and no key is reserved; the bag is never merged into `state`, so nothing in it reaches the graph.",
            "example": {
              "tenant_account_id": "42",
              "dispatch_batch": "nightly-2026-08-25"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/NullableMetadataBag"
              }
            ]
          },
          "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-naturali-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 succeeded 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 succeeded."
          },
          "node_executions": {
            "type": "array",
            "description": "Per-node execution records in chronological order. Each entry captures the resolved input, output, status, and error for a single node execution — the orchestration analogue of an LLM trace.",
            "items": {
              "$ref": "#/components/schemas/NodeExecution"
            }
          },
          "usage": {
            "allOf": [
              {
                "$ref": "#/components/schemas/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 /v1/projects/{project_id}/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 `/v1/projects/{project_id}/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": {
            "type": "object",
            "nullable": true,
            "description": "Error details when status is failed."
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RequiredAction": {
        "type": "object",
        "description": "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-naturali-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-naturali-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-Naturali-Context-<key>` headers on every `http` and `mcp` 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-Naturali-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 (`succeeded`, `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 (succeeded/failed) or awaiting_input state and return the settled run. When false (default), return immediately with status \"queued\" and execute the run in the background."
          }
        }
      },
      "ValidateOrchestrationRequest": {
        "type": "object",
        "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."
          }
        }
      },
      "OrchestrationValidationError": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Location of the issue (e.g. nodes[1].input_mapping.val)."
          },
          "message": {
            "type": "string",
            "description": "Human-readable description of the issue."
          }
        }
      },
      "OrchestrationValidationResult": {
        "type": "object",
        "required": [
          "valid",
          "errors",
          "warnings"
        ],
        "properties": {
          "valid": {
            "type": "boolean",
            "description": "True when there are no blocking errors."
          },
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrchestrationValidationError"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrchestrationValidationError"
            }
          }
        }
      },
      "Project": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public project ID (proj_ prefix).",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "name": {
            "type": "string",
            "example": "acme-perpetual"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ],
            "example": "active"
          },
          "role": {
            "$ref": "#/components/schemas/ProjectRole"
          },
          "owner_user_id": {
            "type": "string",
            "description": "The user who pays for the project — its billing owner, and the one member holding the `owner` role. Membership decides who may act in a project; this decides who is charged for it.\n",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "trace_content_retention_days": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "How long trace and generation content is kept before a daily sweep purges it. `null` (the default) disables retention, so content is kept until purged on demand. A swept record is content-purged the same way an on-demand purge does it — the row survives as an auditable skeleton with `content_redacted_at` set.\n",
            "example": null
          },
          "trace_content_mode": {
            "type": "string",
            "enum": [
              "full",
              "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 this project. `none` is a floor, not a default: an agent may tighten itself to `none`, but cannot loosen a `none` project back to `full`.\n",
            "example": "full"
          },
          "max_concurrent_runs": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Orchestration runs of this project driven at once. `null` (the default) is unlimited. Enforced when a run is claimed — runs past the limit wait for a slot rather than failing.\n",
            "example": null
          },
          "max_chain_generations": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Generations one continuation chain in this project may hold before it stops being resumed. `null` (the default) leaves the platform-wide ceiling in force. The budget actually applied is the smallest of that ceiling, this number, and the agent's own — an agent author can be stricter than this, never looser.\n",
            "example": null
          },
          "max_orchestration_run_depth": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Nesting levels a run tree in this project may reach before the next child is refused. `null` (the default) leaves the platform-wide bound in force; the bound applied is the smaller of the two.\n",
            "example": null
          },
          "require_priced_model": {
            "type": "boolean",
            "description": "Whether a generation whose model carries no price is refused before the provider is called. `false` by default, which runs the model and meters it at no cost. Mainly of use with your own providers — see [Requiring a priced model](/docs/modules/projects#requiring-a-priced-model).\n",
            "example": false
          },
          "guardrail_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Guardrails attached at the project scope — the floor under every tool call by every agent in the project, including tools added later. Empty by default. See [Attaching one](/docs/modules/guardrails#attaching-one).\n",
            "example": []
          },
          "paused_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the project was paused; `null` while it runs. See [Pausing a project](/docs/modules/projects#pausing-a-project).\n",
            "example": null
          },
          "pause_reason": {
            "type": "string",
            "nullable": true,
            "maxLength": 256,
            "description": "The reason the pause named; `null` while the project runs, or when the pause named none.\n",
            "example": null
          },
          "managed_conversion": {
            "type": "boolean",
            "description": "Whether scanned PDFs, images and audio are converted to text on ingest with no setup. `true` by default. See [Managed conversion](/docs/modules/documents#managed-conversion).\n",
            "example": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-17T00:00:00.000Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-17T00:00:00.000Z"
          }
        },
        "required": [
          "id",
          "name",
          "status",
          "role",
          "owner_user_id",
          "trace_content_retention_days",
          "trace_content_mode",
          "max_concurrent_runs",
          "max_chain_generations",
          "max_orchestration_run_depth",
          "require_priced_model",
          "guardrail_ids",
          "managed_conversion",
          "paused_at",
          "pause_reason",
          "created_at",
          "updated_at"
        ]
      },
      "ProjectPause": {
        "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.\n",
            "example": "anomaly detected by the spend monitor"
          }
        }
      },
      "ProjectRole": {
        "type": "string",
        "enum": [
          "owner",
          "admin",
          "member"
        ],
        "description": "What the member may do in this project. `owner` — everything, including deleting it and paying for it; exactly one per project. `admin` — everything a `member` may do, plus managing who else is in the project and how the project itself is configured. `member` — every read and write on the project's own resources, including minting project API keys.\nWhen returned on a `Project`, this is *your* role, not a property of the project: it is what lets a client hide an action the request would refuse.\n",
        "example": "owner"
      },
      "ProjectMember": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public membership ID (pmem_ prefix).",
            "example": "pmem_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "user_id": {
            "type": "string",
            "example": "user_V1StGXR8Z5jdHi6B"
          },
          "email": {
            "type": "string",
            "format": "email",
            "nullable": true,
            "description": "The member's address; null if the account no longer exists.",
            "example": "ana@acme.com"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "example": "Ana Silva"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "pending"
            ],
            "nullable": true,
            "description": "`pending` until the member has signed in for the first time, `active` afterwards. Derived from the account, not stored, so it needs nothing to run when they arrive. Null if the account no longer exists — there is no status to report for somebody who is never arriving.\n",
            "example": "active"
          },
          "role": {
            "$ref": "#/components/schemas/ProjectRole"
          },
          "invited_by_user_id": {
            "type": "string",
            "nullable": true,
            "description": "Who added this member; null for the founding owner, who was added by the act of creating the project.\n",
            "example": null
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-17T00:00:00.000Z"
          }
        },
        "required": [
          "id",
          "project_id",
          "user_id",
          "email",
          "name",
          "status",
          "role",
          "invited_by_user_id",
          "created_at"
        ]
      },
      "ProjectMemberList": {
        "type": "object",
        "description": "The complete member list — unpaginated, because a project's membership is a handful of people, not a collection that grows without bound.\n",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectMember"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "GrantableProjectRole": {
        "type": "string",
        "enum": [
          "admin",
          "member"
        ],
        "description": "The roles a membership write may name. `owner` is absent by construction: it is the billing owner, so granting it would change who pays, and transferring that is a different act with its own decision.\n",
        "example": "member"
      },
      "ProjectMemberCreate": {
        "type": "object",
        "required": [
          "email",
          "role"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "The colleague's address, lower-cased before lookup. It does not need an account yet.\n",
            "example": "ana@acme.com"
          },
          "role": {
            "$ref": "#/components/schemas/GrantableProjectRole"
          }
        }
      },
      "ProjectMemberUpdate": {
        "type": "object",
        "required": [
          "role"
        ],
        "properties": {
          "role": {
            "$ref": "#/components/schemas/GrantableProjectRole"
          }
        }
      },
      "ProjectCreate": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Human-readable project name.",
            "example": "acme-perpetual"
          },
          "idempotency_key": {
            "type": "string",
            "maxLength": 255,
            "description": "Deduplication key, unique within your account, that makes a retry of an ambiguous failure safe. The first request under a key performs the write and answers `201`; any later request carrying the same key answers `200` with that same result, so a timeout you cannot interpret can simply be retried.\n\nThe key is claimed for as long as the record exists and never silently expires. Reusing one with a different request body is `409 idempotency_key_reused` — a key names one request, so changing the body and keeping the key is a bug rather than a retry. A retry that arrives while the original is still in flight is `409 idempotency_request_in_progress`.",
            "example": "signup-2026-09-18-acct-42"
          }
        }
      },
      "ProjectUpdate": {
        "type": "object",
        "description": "At least one field must be present.",
        "minProperties": 1,
        "properties": {
          "name": {
            "type": "string",
            "example": "acme-renamed"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ],
            "example": "archived"
          },
          "trace_content_retention_days": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Days of trace and generation content retention before the daily sweep purges it. Send `null` to disable retention (content is then kept until purged on demand). Omitting the field leaves the current window unchanged — `null` and absent are different instructions.\n\nBounded by your plan: a window longer than the plan allows, and `null` on any plan that sets a window, respond `403` `plan_limit_reached`.\n",
            "example": 90
          },
          "trace_content_mode": {
            "type": "string",
            "enum": [
              "full",
              "none"
            ],
            "description": "Set `none` for zero-retention: content is never written for any agent in this project. Tightening to `none` does not erase content already on disk — purge it explicitly, or set a retention window to have the sweep do it.\n",
            "example": "none"
          },
          "max_concurrent_runs": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Orchestration runs driven at once. Send `null` to lift the limit. Omitting the field leaves it unchanged.\n",
            "example": 5
          },
          "max_chain_generations": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Generations one continuation chain may hold. Send `null` to drop back to the platform-wide ceiling. Omitting the field leaves it unchanged.\n",
            "example": 25
          },
          "max_orchestration_run_depth": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Nesting levels a run tree may reach. Send `null` to drop back to the platform-wide bound. Omitting the field leaves it unchanged.\n",
            "example": 3
          },
          "require_priced_model": {
            "type": "boolean",
            "description": "Refuse a generation whose model carries no price, before the provider is called. Uncapped by plan — it only ever narrows what the project may spend.\n",
            "example": true
          },
          "guardrail_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            },
            "description": "Guardrails attached at the project scope, replaced wholesale. Send `[]` to detach every one; omitting the field leaves them unchanged. Uncapped by plan — a guardrail can only tighten what runs.\n",
            "example": [
              "guard_V1StGXR8Z5jdHi6B"
            ]
          },
          "managed_conversion": {
            "type": "boolean",
            "description": "Turn managed conversion off or back on. Off removes the managed ingestion rules; rules of your own stay.\n",
            "example": false
          }
        }
      },
      "UsageTokens": {
        "type": "object",
        "description": "Token counts (input_tokens already includes cached input).",
        "properties": {
          "input_tokens": {
            "type": "integer",
            "example": 120000
          },
          "output_tokens": {
            "type": "integer",
            "example": 45000
          },
          "cached_tokens": {
            "type": "integer",
            "example": 30000
          },
          "total_tokens": {
            "type": "integer",
            "description": "input_tokens + output_tokens.",
            "example": 165000
          }
        },
        "required": [
          "input_tokens",
          "output_tokens",
          "cached_tokens",
          "total_tokens"
        ]
      },
      "UsageComponent": {
        "type": "object",
        "description": "One measured amount inside a bucket. The token counts describe LLM usage alone, so this is where a bucket metering storage, requests or compute execution reports what it actually measured — without it such a bucket would read as all zeros, indistinguishable from one that measured nothing.\n",
        "properties": {
          "component": {
            "type": "string",
            "description": "The measured dimension — input_tokens, cached_tokens, output_tokens, reasoning_tokens, compute_second, request, gb_day, …\nComponents are not always disjoint. `reasoning_tokens` is the part of `output_tokens` a reasoning model spent thinking: it is reported for visibility and priced as output, so its own `cost_usd` is null to keep it from being counted twice. On a model that reasons by default it can be most of the output — and most of the bill — for a short answer.\n",
            "example": "gb_day"
          },
          "unit": {
            "type": "string",
            "description": "The unit quantity is measured in.",
            "example": "gb_day"
          },
          "quantity": {
            "type": "number",
            "description": "The summed measured amount, in unit. Fractional for measures like GB-days and compute seconds.\n",
            "example": 0.4
          },
          "cost_usd": {
            "type": "number",
            "nullable": true,
            "description": "Billing-grade cost in USD for this component; null when it was not priced, or when another component already prices it (as `output_tokens` prices `reasoning_tokens`). The quantity is still measured — null never means free.\n",
            "example": null
          }
        },
        "required": [
          "component",
          "unit",
          "quantity",
          "cost_usd"
        ]
      },
      "UsageComponents": {
        "type": "object",
        "description": "The measured amounts in a bucket, one entry per component.",
        "properties": {
          "components": {
            "type": "array",
            "description": "Sorted by component, then unit. Empty when nothing was measured in the bucket.\n",
            "items": {
              "$ref": "#/components/schemas/UsageComponent"
            }
          }
        },
        "required": [
          "components"
        ]
      },
      "UsageGroup": {
        "allOf": [
          {
            "type": "object",
            "properties": {
              "key": {
                "type": "string",
                "nullable": true,
                "description": "The bucket's value in the chosen dimension (model, meter type, source, AI provider/agent/run/session/actor id, or YYYY-MM-DD day); null when it does not apply. Under `group_by=model` this is the model name, the same string `GET /v1/models` lists and an agent is configured with; under `group_by=ai_provider` it is the provider id.\n",
                "example": "nova-lite-v1"
              },
              "ai_provider_id": {
                "type": "string",
                "nullable": true,
                "description": "The AI provider that served the bucket's model, under `group_by=model`; null on every other dimension, including `group_by=ai_provider`, where the provider is `key`. The model dimension buckets on the model *and* its provider, so one model served by two providers is two groups — this is what tells them apart. The groups still sum to the totals.\n",
                "example": "aip_V1StGXR8Z5jdHi6B"
              },
              "cost_usd": {
                "type": "number",
                "nullable": true,
                "description": "Billing-grade cost in USD; null when nothing in the bucket was priced.",
                "example": 1.23
              },
              "event_count": {
                "type": "integer",
                "description": "Metered events in the bucket. Cost alone does not say whether a bucket is one expensive call or a thousand cheap ones, and it is not a count of generations: one generation writes an event per dimension it meters.\n",
                "example": 42
              }
            },
            "required": [
              "key",
              "ai_provider_id",
              "cost_usd",
              "event_count"
            ]
          },
          {
            "$ref": "#/components/schemas/UsageTokens"
          },
          {
            "$ref": "#/components/schemas/UsageComponents"
          }
        ]
      },
      "ProjectUsageGroups": {
        "type": "object",
        "description": "One page of buckets, one entry per distinct value in the chosen dimension. The rollup grows with the window — day buckets per day, model per model served — so the buckets are paged while total counts the whole window. Ordered by cost_usd descending, ties broken by key then ai_provider_id ascending, nulls last: the first page is the biggest spenders, and paging never repeats or skips a bucket.\n",
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UsageGroup"
            }
          },
          "total": {
            "type": "integer",
            "description": "Distinct buckets in the window, independent of limit, including a null bucket for the events the chosen dimension does not apply to. It is a count of buckets and not of anything they describe: under group_by=run a project that runs no orchestration has exactly one bucket whatever its volume. How many runs an account made is GET /v1/users/me/usage.\n",
            "example": 12
          },
          "limit": {
            "type": "integer",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "example": 0
          }
        }
      },
      "UsageEventComponent": {
        "type": "object",
        "description": "One priced dimension of a usage event. Every meter type is expressed as components, so tokens and infra read uniformly: an llm_tokens event carries input_tokens / output_tokens (and cached_tokens, plus a non-billable reasoning_tokens detail), a compute_execution event one compute_second.\n",
        "properties": {
          "component": {
            "type": "string",
            "description": "The measured dimension — input_tokens, compute_second, gb_day, …",
            "example": "input_tokens"
          },
          "quantity": {
            "type": "number",
            "description": "The measured amount, in unit.",
            "example": 1200
          },
          "unit": {
            "type": "string",
            "example": "token"
          },
          "billable": {
            "type": "boolean",
            "description": "Whether this component contributes to cost. Non-billable details — reasoning_tokens, which is a subset of output_tokens — are never priced and never double-counted into the 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 x unit_price, frozen at write time; null when unpriced."
          },
          "price_id": {
            "type": "string",
            "nullable": true,
            "description": "The price-book row that priced this component."
          }
        },
        "required": [
          "component",
          "quantity",
          "unit"
        ]
      },
      "ProjectUsageEvent": {
        "type": "object",
        "description": "One metered occurrence — a completed model call, a node execution, a storage sample. Attribution and total cost live here; the measured amounts live in components.\n",
        "properties": {
          "id": {
            "type": "string",
            "example": "ue_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "x-naturali-ref": "projects"
          },
          "generation_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "generations"
          },
          "trace_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "traces"
          },
          "agent_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "agents"
          },
          "actor_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "actors",
            "description": "The end user the 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": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "sessions",
            "description": "The session the occurrence ran in, frozen at write time. Null for work not dispatched through a session.\n"
          },
          "orchestration_run_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "orchestration-runs"
          },
          "node_id": {
            "type": "string",
            "nullable": true,
            "description": "The orchestration node within the run, when applicable."
          },
          "ai_provider_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "ai-providers",
            "description": "The provider billed — the target a model route picked for the turn, or the agent's pinned provider. Null if the provider was since deleted; the provider/model snapshot still records what was billed.\n"
          },
          "trigger_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "triggers"
          },
          "action_id": {
            "type": "string",
            "nullable": true,
            "description": "The caller-supplied action label, when one was given."
          },
          "meter_type": {
            "type": "string",
            "description": "llm_tokens, compute_execution, api_request, storage or tool_execution.",
            "example": "llm_tokens"
          },
          "source": {
            "type": "string",
            "nullable": true,
            "description": "What the spend was incurred for. eval is an eval run's item generations and eval_judge an llm_judge scorer's own completion, so running a suite is priced apart from grading it. embedding is any embedding call. Null for ordinary agent traffic.\n",
            "example": null
          },
          "provider": {
            "type": "string",
            "nullable": true,
            "description": "The vendor the SKU was billed against, retained even if the provider record is deleted. Platform meters — storage, requests, compute execution — are billed by naturali itself and read \"naturali\".\n",
            "example": "bedrock"
          },
          "model": {
            "type": "string",
            "nullable": true,
            "description": "The model, or the billable SKU for a platform meter. On the managed offering this is the same public name GET /v1/models lists; a provider you brought yourself keeps the string that vendor uses.\n",
            "example": "nova-lite-v1"
          },
          "cost_usd": {
            "type": "number",
            "nullable": true,
            "description": "Total USD cost, the sum of the priced components, frozen at write time. Null when nothing was priced.\n"
          },
          "components": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UsageEventComponent"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "project_id",
          "meter_type",
          "provider",
          "model",
          "cost_usd",
          "components",
          "created_at"
        ]
      },
      "ProjectUsageEventPage": {
        "type": "object",
        "description": "One page of events. total counts every event the narrowing matched, independent of limit.\n",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectUsageEvent"
            }
          },
          "total": {
            "type": "integer",
            "example": 1280
          },
          "limit": {
            "type": "integer",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "ProjectUsageReceipt": {
        "type": "object",
        "description": "The itemisation of one generation or one orchestration run. Exactly one of generation_id / orchestration_run_id is present, matching the selector that was asked for.\n",
        "properties": {
          "generation_id": {
            "type": "string",
            "x-naturali-ref": "generations",
            "description": "Present on a per-generation receipt."
          },
          "orchestration_run_id": {
            "type": "string",
            "x-naturali-ref": "orchestration-runs",
            "description": "Present on an orchestration-run receipt."
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "line_items": {
            "type": "array",
            "description": "One line per usage event — for a generation receipt the events on that generation, for a run receipt every event across the run.\n",
            "items": {
              "type": "object",
              "properties": {
                "event_id": {
                  "type": "string",
                  "example": "ue_V1StGXR8Z5jdHi6B"
                },
                "meter_type": {
                  "type": "string",
                  "example": "llm_tokens"
                },
                "provider": {
                  "type": "string",
                  "nullable": true,
                  "description": "As on an event: the vendor billed, or \"naturali\" for a platform meter.\n",
                  "example": "bedrock"
                },
                "model": {
                  "type": "string",
                  "nullable": true,
                  "description": "As on an event, with one caveat: a line carries no provider of its own, so the public name is resolved by reading the events behind the receipt. Where that read cannot cover every line, all of them keep the string the meter recorded rather than some being renamed and some not.\n",
                  "example": "nova-lite-v1"
                },
                "node_id": {
                  "type": "string",
                  "nullable": true,
                  "description": "The orchestration node that produced the event. On a run receipt every line carries it, so grouping by node_id gives the per-node cost the total hides. A retried node contributes one line per attempt — a retry is real money. Null when no node produced the event.\n"
                },
                "cost_usd": {
                  "type": "number",
                  "nullable": true
                },
                "components": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/UsageEventComponent"
                  }
                }
              },
              "required": [
                "event_id",
                "meter_type",
                "provider",
                "model",
                "cost_usd",
                "components"
              ]
            }
          },
          "by_meter_type": {
            "type": "array",
            "description": "Per-meter-type cost rollup — the tokens-and-infra split. A single-type receipt has one entry whose cost equals the total.\n",
            "items": {
              "type": "object",
              "properties": {
                "meter_type": {
                  "type": "string"
                },
                "cost_usd": {
                  "type": "number",
                  "nullable": true
                }
              },
              "required": [
                "meter_type",
                "cost_usd"
              ]
            }
          },
          "totals": {
            "type": "object",
            "description": "Token counts and cost summed across the line items.",
            "properties": {
              "cost_usd": {
                "type": "number",
                "nullable": true,
                "description": "Sum of the priced components; null when nothing was priced."
              },
              "input_tokens": {
                "type": "integer",
                "description": "Full prompt tokens, cached input included."
              },
              "output_tokens": {
                "type": "integer"
              },
              "cached_tokens": {
                "type": "integer"
              },
              "reasoning_tokens": {
                "type": "integer",
                "description": "The part of output_tokens a reasoning model spent thinking. Reported for visibility and priced as output, so it is never counted twice.\n"
              }
            }
          }
        },
        "required": [
          "currency",
          "line_items",
          "by_meter_type",
          "totals"
        ]
      },
      "UsageThreshold": {
        "type": "object",
        "description": "An alert on this project's windowed usage.",
        "properties": {
          "id": {
            "type": "string",
            "example": "uth_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "x-naturali-ref": "projects"
          },
          "metric": {
            "type": "string",
            "enum": [
              "cost_usd",
              "tokens"
            ],
            "description": "What is measured — cost_usd across every meter type, or tokens (input + output + cached).\n"
          },
          "window": {
            "type": "string",
            "enum": [
              "calendar_month",
              "rolling_24h"
            ],
            "description": "The current UTC calendar month, or the trailing 24 hours.\n"
          },
          "threshold": {
            "type": "number",
            "description": "The value the windowed aggregate must cross to fire.",
            "example": 250
          },
          "last_fired_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When it last fired; null until the first time."
          },
          "fired_window_key": {
            "type": "string",
            "nullable": true,
            "description": "The YYYY-MM window of the last fire, which is what keeps a calendar_month threshold to one alert per month. Null for rolling_24h and before the first fire.\n",
            "example": null
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "project_id",
          "metric",
          "window",
          "threshold"
        ]
      },
      "UsageThresholdCreate": {
        "type": "object",
        "properties": {
          "metric": {
            "type": "string",
            "enum": [
              "cost_usd",
              "tokens"
            ]
          },
          "window": {
            "type": "string",
            "enum": [
              "calendar_month",
              "rolling_24h"
            ]
          },
          "threshold": {
            "type": "number",
            "description": "Must be greater than zero.",
            "example": 250
          }
        },
        "required": [
          "metric",
          "window",
          "threshold"
        ]
      },
      "ProjectUsageFilters": {
        "type": "object",
        "description": "Every narrowing this meter knows, 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 narrowed\" from \"this meter does not know that narrowing\". Grouped rather than spread across the top level: at twelve they would outnumber the figures, and a top-level ai_provider_id would sit beside a per-bucket ai_provider_id that means something else.\n\nThe eight naming a resource (session_id, actor_id, agent_id, ai_provider_id, orchestration_run_id, orchestration_id, generation_id, trace_id) are resolved against the project and empty the rollup when they name nothing in it; the rest are matched against the value the event recorded.\n\nmeter_type, session_id and actor_id also appear at the top level, where they were the response's only narrowing echo before the others existed.\n",
        "properties": {
          "meter_type": {
            "type": "string",
            "nullable": true
          },
          "session_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "sessions"
          },
          "actor_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "actors"
          },
          "agent_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "agents"
          },
          "ai_provider_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "ai-providers"
          },
          "orchestration_run_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "orchestration-runs"
          },
          "orchestration_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "orchestrations"
          },
          "generation_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "generations"
          },
          "trace_id": {
            "type": "string",
            "nullable": true,
            "x-naturali-ref": "traces"
          },
          "source": {
            "type": "string",
            "nullable": true
          },
          "trigger_id": {
            "type": "string",
            "nullable": true
          },
          "action_id": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "meter_type",
          "session_id",
          "actor_id",
          "agent_id",
          "ai_provider_id",
          "orchestration_run_id",
          "orchestration_id",
          "generation_id",
          "trace_id",
          "source",
          "trigger_id",
          "action_id"
        ]
      },
      "ProjectUsageDistinct": {
        "type": "object",
        "nullable": true,
        "description": "Present only when include=distinct was sent; null otherwise — never zeroes for a rollup that did not compute it, because a counter nobody asked for must not read as \"none\".\n\nHow many distinct entities of each kind the window touched — the counters a \"how many generations / runs / end users this cycle\" question reads, and what groups.total is not: bucket cardinality counts buckets, and every dimension keeps a null bucket for the events it does not apply to.\n\nNulls are not counted here: work with no end user behind it contributes to event_count and to neither actors nor sessions, and a standalone generation counts under generations and not under orchestration_runs.\n\nNone of these add up. Two adjacent windows' sessions overlap wherever a session spans the boundary, and a run that straddles midnight is in both days. A wider figure is a wider query, never a sum of narrower ones.\n",
        "properties": {
          "generations": {
            "type": "integer",
            "example": 812
          },
          "traces": {
            "type": "integer"
          },
          "orchestration_runs": {
            "type": "integer"
          },
          "agents": {
            "type": "integer"
          },
          "actors": {
            "type": "integer"
          },
          "sessions": {
            "type": "integer"
          },
          "ai_providers": {
            "type": "integer"
          }
        }
      },
      "ProjectUsage": {
        "allOf": [
          {
            "type": "object",
            "properties": {
              "project_id": {
                "type": "string",
                "x-naturali-ref": "projects",
                "example": "proj_V1StGXR8Z5jdHi6B"
              },
              "window": {
                "type": "object",
                "description": "The [from, to] bounds applied, echoed back (null = unbounded).",
                "properties": {
                  "from": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "example": null
                  },
                  "to": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "example": null
                  }
                },
                "required": [
                  "from",
                  "to"
                ]
              },
              "group_by": {
                "type": "string",
                "enum": [
                  "model",
                  "ai_provider",
                  "agent",
                  "run",
                  "day",
                  "meter_type"
                ],
                "example": "model"
              },
              "meter_type": {
                "type": "string",
                "nullable": true,
                "description": "The meter-type filter applied, echoed back; null when unfiltered.",
                "example": null
              },
              "session_id": {
                "type": "string",
                "nullable": true,
                "x-naturali-ref": "sessions",
                "description": "The session the rollup was narrowed to, echoed back; null when unnarrowed. Always present, so a caller can tell an unnarrowed rollup from one this meter did not narrow.\n",
                "example": null
              },
              "actor_id": {
                "type": "string",
                "nullable": true,
                "x-naturali-ref": "actors",
                "description": "The actor the rollup was narrowed to, echoed back; null when unnarrowed.",
                "example": null
              },
              "filters": {
                "$ref": "#/components/schemas/ProjectUsageFilters"
              },
              "distinct": {
                "$ref": "#/components/schemas/ProjectUsageDistinct"
              },
              "event_count": {
                "type": "integer",
                "description": "Metered events in the whole window, however many buckets were paged through.\n",
                "example": 1280
              },
              "cost_usd": {
                "type": "number",
                "nullable": true,
                "description": "Total billing-grade cost in USD; null when nothing was priced.",
                "example": 1.23
              },
              "groups": {
                "$ref": "#/components/schemas/ProjectUsageGroups"
              }
            },
            "required": [
              "project_id",
              "window",
              "group_by",
              "meter_type",
              "session_id",
              "actor_id",
              "filters",
              "distinct",
              "event_count",
              "cost_usd",
              "groups"
            ]
          },
          {
            "$ref": "#/components/schemas/UsageTokens"
          },
          {
            "$ref": "#/components/schemas/UsageComponents"
          }
        ]
      },
      "ProjectList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Project"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Cursor for the next page, or null at the end.",
            "example": null
          }
        }
      },
      "Quota": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "quota_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-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-naturali-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-naturali-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-naturali-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
          },
          "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-naturali-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-naturali-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-naturali-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"
          },
          "actor_id": {
            "x-naturali-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-Naturali-Context-<key>` headers on every `http` and `mcp` tool call in this session. The header name is the deployment's configured context prefix (`X-Naturali-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
          }
        }
      },
      "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-Naturali-Context-<key>` headers on every `http` and `mcp` tool call in this session. The header name is the deployment's configured context prefix (`X-Naturali-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
          }
        }
      },
      "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-Naturali-Context-<key>` headers on every `http` and `mcp` tool call in this generation. The header name is the deployment's configured context prefix (`X-Naturali-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.\n",
                "example": "wamid.HBgLNTUxMTk4..."
              }
            }
          },
          {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "document_id"
            ],
            "properties": {
              "document_id": {
                "x-naturali-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-Naturali-Context-<key>` headers on every `http` and `mcp` tool call in this generation. The header name is the deployment's configured context prefix (`X-Naturali-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.\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-naturali-ref": "documents",
            "type": "string",
            "nullable": true
          }
        }
      },
      "AddSessionMessageResponse": {
        "anyOf": [
          {
            "$ref": "#/components/schemas/AddSessionMessageSaved"
          },
          {
            "$ref": "#/components/schemas/GenerateSessionResponse"
          }
        ]
      },
      "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-Naturali-Context-<key>` headers on every `http` and `mcp` tool call in this generation. The header name is the deployment's configured context prefix (`X-Naturali-Context-` by default) plus the key verbatim — no character is re-cased. Keys are never case-converted — they round-trip exactly as sent. An invalid or colliding key is rejected with `400 INVALID_TOOL_CONTEXT_KEY`."
          }
        }
      },
      "GenerateSessionResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "completed",
              "requires_action"
            ]
          },
          "message": {
            "type": "object",
            "properties": {
              "role": {
                "type": "string"
              },
              "content": {
                "type": "string"
              },
              "model": {
                "type": "string"
              }
            }
          },
          "generation_id": {
            "x-naturali-ref": "generations",
            "type": "string"
          },
          "trace_id": {
            "x-naturali-ref": "traces",
            "type": "string"
          },
          "required_action": {
            "type": "object",
            "description": "Present when status is requires_action",
            "properties": {
              "tool_calls": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "tool_name": {
                      "type": "string"
                    },
                    "args": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "SendSessionMessageResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "completed",
              "requires_action"
            ]
          },
          "message": {
            "type": "object",
            "properties": {
              "role": {
                "type": "string"
              },
              "content": {
                "type": "string"
              },
              "model": {
                "type": "string"
              }
            }
          },
          "generation_id": {
            "x-naturali-ref": "generations",
            "type": "string"
          },
          "trace_id": {
            "x-naturali-ref": "traces",
            "type": "string"
          },
          "required_action": {
            "type": "object",
            "description": "Present when status is requires_action",
            "properties": {
              "tool_calls": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "tool_name": {
                      "type": "string"
                    },
                    "args": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "SubmitSessionToolOutputsRequest": {
        "type": "object",
        "required": [
          "generation_id",
          "tool_outputs"
        ],
        "additionalProperties": false,
        "properties": {
          "generation_id": {
            "x-naturali-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"
                }
              }
            }
          }
        }
      },
      "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. Null when the task was created without any. Unlike `payload` it is invisible to guards and to `payload_writes`, so it is the place for an attribution label rather than task data.",
            "example": {
              "tenant_account_id": "42",
              "source": "zendesk"
            },
            "allOf": [
              {
                "$ref": "#/components/schemas/NullableMetadataBag"
              }
            ]
          },
          "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_kind": {
            "type": "string",
            "description": "Who made the move. `user` and `api_key` are authenticated principals; `automation` (the engine acting on an `on_enter` dispatch outcome) and `approval` (an approval resolution) are system principals. Named `principal_*`, not `actor_*`: these ids never reference the Actors module.\n",
            "enum": [
              "user",
              "api_key",
              "automation",
              "approval"
            ]
          },
          "principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public id of the principal that made the move — the user (`user_...`), or for `api_key` auth the key's own id (`key_...`), distinguishing which key acted. Null for `automation`, which has no principal: the cause is carried by `generation_id` / `orchestration_run_id` / `tool_id`, one per dispatch kind — exactly one of which is set on an automation move.\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
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateTaskRequest": {
        "type": "object",
        "required": [
          "workflow_id",
          "title"
        ],
        "additionalProperties": false,
        "properties": {
          "workflow_id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "payload": {
            "type": "object"
          },
          "assignee": {
            "type": "string",
            "nullable": true
          },
          "state": {
            "type": "string",
            "description": "Name of a declared workflow state to create the task in directly, instead of the workflow's `initial` state. Must name a state declared on the workflow, or the request is rejected with `TASK_STATE_NOT_FOUND` (400). Defaults to the `initial` state."
          },
          "tool_context": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Key-value pairs forwarded as `X-Naturali-Context-<key>` headers on every `http` and `mcp` 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-Naturali-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
      },
      "TransitionTaskRequest": {
        "type": "object",
        "required": [
          "transition"
        ],
        "additionalProperties": false,
        "properties": {
          "transition": {
            "type": "string"
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "tool_context": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Caller context for the automation dispatches the task makes from here on, forwarded as `X-Naturali-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-naturali-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",
              "pipeline"
            ],
            "description": "Tool type",
            "example": "http"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "What the tool does (sent to the model)"
          },
          "parameters": {
            "type": "object",
            "nullable": true,
            "description": "JSON Schema for tool input"
          },
          "execute": {
            "type": "object",
            "nullable": true,
            "description": "Execution config for http tools. Supported fields: `url` (required), `method` (default `POST`), `headers`, and `body_mode`. The `url` may contain `{paramName}` placeholders (e.g. `/users/{userId}`) that are replaced at call time with the corresponding tool argument value (URL-encoded). Arguments consumed as path parameters are excluded from the query string and request body. `body_mode` is `json` (default) or `multipart`. In `multipart` mode the merged tool arguments are sent as a `multipart/form-data` body: scalar fields become plain form fields and a field shaped like `{ content_type, filename, data_base64 }` is decoded from base64 and attached as a file part (the hardcoded `Content-Type: application/json` is dropped so `fetch` sets the multipart boundary itself).\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 written as a **literal** is stored and still sent on every call, but it is masked on every read: `secret_access_key`, `session_token` and `credentials` under `auth`, and any credential-named `headers` value (`Authorization`, `Cookie`, or a name containing `api-key`/`token`/`secret`/`password`), come back as `{\"no_echo\": true}`. The mask is 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` is masked on read, exactly as in `execute`."
          },
          "actions": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "Allowlist of actions to expose. 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-Naturali-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.\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."
          },
          "output_mapping": {
            "type": "object",
            "nullable": true,
            "description": "Universal JSON Logic mapping applied to the tool's raw result, for every tool type (`http`, `mcp`, `pipeline`, `client`). Evaluated over `{ output: <raw result>, input: <merged input> }`, so `{ \"var\": \"output.text\" }` extracts a bare scalar field instead of requiring a wrapping `pipeline` tool, and `{ \"var\": \"input.title\" }` echoes back a field of the request that produced the response. For `pipeline` tools this runs *after* the pipeline's own `output` mapping, over that mapping's result."
          },
          "guardrail_ids": {
            "x-naturali-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"
          }
        }
      },
      "UpdateToolRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "http",
              "client",
              "mcp",
              "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 `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-Naturali-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.\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-naturali-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 `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-Naturali-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"
            }
          }
        }
      },
      "Trace": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public ID of the trace",
            "example": "trace_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "x-naturali-ref": "projects",
            "type": "string",
            "description": "Public ID of the project"
          },
          "agent_id": {
            "x-naturali-ref": "agents",
            "type": "string",
            "description": "Public ID of the agent that produced this trace"
          },
          "file_id": {
            "x-naturali-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-naturali-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-naturali-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": {
            "type": "object",
            "nullable": true,
            "description": "Structured error payload recorded when a generation in this trace failed (e.g. an upstream AI provider error). Null if no failure has been recorded.\n",
            "properties": {
              "code": {
                "type": "string",
                "example": "AI_PROVIDER_ERROR"
              },
              "message": {
                "type": "string",
                "example": "Provider returned 402: insufficient credits"
              },
              "meta": {
                "type": "object"
              }
            }
          },
          "content_redacted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the trace's content was purged. Non-null means the steps object has been deleted from storage and the content columns cleared, while this row survives as an auditable skeleton (ids, timestamps, step count). A purged trace still reads back as a skeleton with this marker set rather than as a 404, so the erasure is provable.\n"
          },
          "content_redacted_by_principal_type": {
            "type": "string",
            "nullable": true,
            "description": "Principal kind that purged the content ('user' or 'api_key')",
            "example": "user"
          },
          "content_redacted_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public ID of the principal that purged the content — the API key's own id for key auth, so the record names which key acted.\n"
          },
          "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-naturali-ref": "projects",
            "type": "string"
          },
          "agent_id": {
            "x-naturali-ref": "agents",
            "type": "string"
          },
          "file_id": {
            "x-naturali-ref": "files",
            "type": "string",
            "nullable": true
          },
          "step_count": {
            "type": "integer"
          },
          "parent_trace_id": {
            "x-naturali-ref": "traces",
            "type": "string",
            "nullable": true
          },
          "root_trace_id": {
            "x-naturali-ref": "traces",
            "type": "string",
            "nullable": true
          },
          "error": {
            "type": "object",
            "nullable": true,
            "description": "Structured error payload recorded when a generation in this trace failed"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "content_redacted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the trace's content was purged. Non-null means the steps object has been deleted from storage and the content columns cleared, while this row survives as an auditable skeleton (ids, timestamps, step count). A purged trace still reads back as a skeleton with this marker set rather than as a 404, so the erasure is provable.\n"
          },
          "content_redacted_by_principal_type": {
            "type": "string",
            "nullable": true,
            "description": "Principal kind that purged the content ('user' or 'api_key')",
            "example": "user"
          },
          "content_redacted_by_principal_id": {
            "type": "string",
            "nullable": true,
            "description": "Public ID of the principal that purged the content — the API key's own id for key auth, so the record names which key acted.\n"
          },
          "children": {
            "type": "array",
            "description": "Child traces triggered by sub-agent calls from this trace",
            "items": {
              "$ref": "#/components/schemas/TraceTreeNode"
            }
          },
          "generations": {
            "type": "array",
            "description": "Generations that belong to this trace node. Only present when `include=generations` is requested. Includes top-level generations and sub-agent child generations linked via `initiator_generation_id`.\n",
            "items": {
              "$ref": "#/components/schemas/Generation"
            }
          }
        }
      },
      "Trigger": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "project_id": {
            "x-naturali-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 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-naturali-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": {
          "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 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-Naturali-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-naturali-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-naturali-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-naturali-ref": "triggers",
            "type": "string"
          },
          "project_id": {
            "x-naturali-ref": "projects",
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "manual",
              "webhook",
              "schedule",
              "event"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "running",
              "succeeded",
              "failed"
            ]
          },
          "input": {
            "type": "object",
            "nullable": true
          },
          "result": {
            "type": "object",
            "nullable": true,
            "description": "{ target_type, result_id, status, output } — output truncated"
          },
          "error": {
            "type": "object",
            "nullable": true,
            "description": "{ code, message, meta }"
          },
          "idempotency_key": {
            "type": "string",
            "nullable": true,
            "description": "`<event_id>:<trigger_id>` for an `event` firing, 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"
          }
        }
      },
      "UserUpdate": {
        "type": "object",
        "minProperties": 1,
        "description": "Send at least one field.",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Display name; null clears it.",
            "example": "Ana Silva"
          },
          "usage_alerts": {
            "type": "boolean",
            "description": "Email the account at 75%, 90% and 100% of its model credit, runs and indexed storage.",
            "example": true
          }
        }
      },
      "UserBilling": {
        "type": "object",
        "required": [
          "plan",
          "next_plan",
          "retention_days",
          "credit_balance_usd",
          "earned_balance_usd",
          "paid_balance_usd",
          "spend_reconciled_at",
          "storage_gb",
          "storage_limit_gb",
          "storage_sampled_at"
        ],
        "properties": {
          "plan": {
            "type": "string",
            "description": "The rung of the infra subscription in effect for this account: the highest it held this month. `free` for an account with no subscription, which is also the least-privileged rung. It gates the feature lines and the resource counts of the pricing table.",
            "enum": [
              "free",
              "pro",
              "business",
              "enterprise"
            ]
          },
          "next_plan": {
            "type": "string",
            "description": "The rung the account moves to when the month turns. It differs from `plan` only while a downgrade is pending: a downgrade takes effect at the end of the month, an upgrade at once.",
            "enum": [
              "free",
              "pro",
              "business",
              "enterprise"
            ],
            "example": "pro"
          },
          "retention_days": {
            "type": "integer",
            "nullable": true,
            "description": "The longest window a project of this account may keep trace and generation content for, in days. A `PATCH` asking for a wider one — `null` included, which keeps content indefinitely — is refused with `403 plan_limit_reached`; anything shorter is always allowed. On a plan whose window a contract sets, this is that contract's figure, and `null` means nothing bounds it. A plan change that shortens the window applies at the end of the billing cycle, so this reads as the widest window held during the current one.",
            "example": 30
          },
          "credit_balance_usd": {
            "type": "number",
            "description": "Model credit left, in US dollars — granted and purchased credit plus the plan's monthly allowance, less managed-model and embedding spend. Managed generations and writes that embed are refused while this is negative; zero is not. Only as current as `spend_reconciled_at`, so read the two together.",
            "example": 18.42
          },
          "earned_balance_usd": {
            "type": "number",
            "description": "Marketplace earnings: the publisher's share of fees its consumers funded, less withdrawals. Never lapses.",
            "example": 0
          },
          "paid_balance_usd": {
            "type": "number",
            "description": "What may fund a marketplace fee: purchased credit plus earnings, less the usage included and granted credit did not cover and the fees paid. Included and granted credit never fund a fee, so a call to a priced installed tool or agent is refused with `402 insufficient_credit` while this is not positive.",
            "example": 0
          },
          "spend_reconciled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When spend was last read off the meter and written to the ledger. `null` when it has never been read for at least one of the account's projects, or the account has none — a balance whose spend has never been read is not current as of anything.",
            "example": "2026-07-17T00:15:00.000Z"
          },
          "storage_gb": {
            "type": "number",
            "description": "Indexed storage the account is holding, in the metered gigabytes the ceiling is enforced in — the raw file plus every chunk's text and its embedding vector, so not the size of what you uploaded.\n\n**The account's total, not a project's.** The ceiling is pooled across every project the account pays for, so one project may hold all of it. Per project, read the `gb_day` component of `GET /v1/projects/{project_id}/usage`.",
            "example": 12.4137
          },
          "storage_limit_gb": {
            "type": "number",
            "nullable": true,
            "description": "The pool the account's plan allows. An ingest past it is refused with `403 plan_limit_reached` and `resource: \"storage\"`, whose `details` carry this figure as `limit` and what you are holding as `storage_gb`. Null where a contract sets the ceiling, or where the plan sets none.",
            "example": 30
          },
          "storage_sampled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the **oldest** of the samples behind `storage_gb` was taken — the total is only as current as its stalest part. Measured every few minutes per project rather than on this request, so read the two together, exactly as with the balance and `spend_reconciled_at`.\n\nNull when a project the account pays for has never been sampled, or it has no projects: a total missing a component is not current as of anything. The figure itself still reports what the samples that do exist add up to.",
            "example": "2026-07-17T00:15:00.000Z"
          }
        }
      },
      "UserUsage": {
        "type": "object",
        "required": [
          "cycle_from",
          "runs",
          "runs_included",
          "projects"
        ],
        "properties": {
          "cycle_from": {
            "type": "string",
            "format": "date-time",
            "description": "Start of the billing cycle the runs were counted over — the first instant of the current UTC calendar month.",
            "example": "2026-07-01T00:00:00.000Z"
          },
          "runs": {
            "type": "integer",
            "nullable": true,
            "description": "Runs this cycle across every project the account pays for. `null` when at least one project's meter could not be read: a total missing a project would understate it, and an understated count read against an allowance claims headroom that may not exist. The per-project breakdown names which one.",
            "example": 1284
          },
          "runs_included": {
            "type": "integer",
            "nullable": true,
            "description": "Runs the account's plan includes per cycle. `null` on a contract plan, where the figure is commercial rather than published. Exceeding it is refused on `free` and billed as overage on `pro` and `business`.",
            "example": 2000
          },
          "projects": {
            "type": "array",
            "description": "Per project, so a total that has spent its allowance can be traced to what spent it. Empty for an account with no projects.",
            "items": {
              "$ref": "#/components/schemas/ProjectRuns"
            }
          }
        }
      },
      "ProjectRuns": {
        "type": "object",
        "required": [
          "project_id",
          "runs"
        ],
        "properties": {
          "project_id": {
            "type": "string",
            "x-naturali-ref": "projects",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "runs": {
            "type": "integer",
            "nullable": true,
            "description": "Runs this cycle; `null` when this project's meter could not be read.",
            "example": 1284
          }
        }
      },
      "SpendStop": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "kind",
          "resource_id",
          "reason",
          "restorable",
          "stopped_at",
          "resolution",
          "resolved_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "sst_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "kind": {
            "type": "string",
            "enum": [
              "schedule_trigger",
              "eval_run",
              "orchestration_run",
              "workflow_task"
            ],
            "description": "What was stopped: a schedule trigger is disabled, an eval run cancelled, an orchestration run or a workflow task's automation paused."
          },
          "resource_id": {
            "type": "string",
            "description": "The stopped trigger, eval run, orchestration run or task.",
            "example": "trg_V1StGXR8Z5jdHi6B"
          },
          "reason": {
            "type": "string",
            "enum": [
              "debt",
              "run_allowance",
              "marketplace_fee"
            ],
            "description": "The ceiling that stopped it: a credit balance below zero, a Free plan's runs for the month used up, or marketplace fees beyond purchased credit (`paid_balance_usd` below zero)."
          },
          "restorable": {
            "type": "boolean",
            "description": "False for a cancelled eval run, which can only be dismissed."
          },
          "stopped_at": {
            "type": "string",
            "format": "date-time"
          },
          "resolution": {
            "type": "string",
            "enum": [
              "restored",
              "dismissed"
            ],
            "nullable": true
          },
          "resolved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "SpendStopPage": {
        "type": "object",
        "required": [
          "data",
          "blocked_by",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SpendStop"
            }
          },
          "blocked_by": {
            "type": "string",
            "enum": [
              "debt",
              "run_allowance",
              "marketplace_fee"
            ],
            "nullable": true,
            "description": "The ceiling still closed, which blocks every restore; null when none is."
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "StatementLine": {
        "type": "object",
        "required": [
          "kind",
          "plan",
          "description",
          "period_from",
          "period_to",
          "quantity",
          "unit",
          "unit_price_usd",
          "amount_usd"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "subscription",
              "run_overage",
              "prepaid",
              "model_credit_spend",
              "marketplace_fees",
              "marketplace_earnings",
              "platform_take"
            ],
            "description": "`subscription`, `run_overage` and `prepaid` make up `total_usd` and are charged; `prepaid` is fee already paid by card at an upgrade, netted as a negative amount. The other four report movements of prepaid credit in the cycle and are not part of it."
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "business",
              "enterprise"
            ]
          },
          "description": {
            "type": "string",
            "example": "Pro plan, 2026-10-01 to 2026-10-31"
          },
          "period_from": {
            "type": "string",
            "format": "date-time"
          },
          "period_to": {
            "type": "string",
            "format": "date-time",
            "description": "Exclusive."
          },
          "quantity": {
            "type": "number",
            "description": "For `subscription`, the share of the month the plan was held; for `run_overage`, the runs past the allowance; for `prepaid`, 1.",
            "example": 1
          },
          "unit": {
            "type": "string",
            "enum": [
              "month",
              "1,000 runs",
              "payment",
              "USD"
            ]
          },
          "unit_price_usd": {
            "type": "number",
            "example": 49
          },
          "amount_usd": {
            "type": "number",
            "example": 49
          }
        }
      },
      "Statement": {
        "type": "object",
        "required": [
          "id",
          "cycle",
          "period_from",
          "period_to",
          "currency",
          "runs",
          "runs_included",
          "lines",
          "total_usd",
          "payment_status",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "stm_V1StGXR8Z5jdHi6B"
          },
          "cycle": {
            "type": "string",
            "description": "The billing month, `YYYY-MM` in UTC.",
            "example": "2026-10"
          },
          "period_from": {
            "type": "string",
            "format": "date-time"
          },
          "period_to": {
            "type": "string",
            "format": "date-time",
            "description": "Exclusive."
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD"
            ]
          },
          "runs": {
            "type": "integer",
            "description": "Runs that month across every project the account pays for.",
            "example": 30000
          },
          "runs_included": {
            "type": "integer",
            "nullable": true,
            "description": "The allowance overage was measured against.",
            "example": 25000
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatementLine"
            }
          },
          "total_usd": {
            "type": "number",
            "description": "What is owed, after anything prepaid.",
            "example": 59
          },
          "payment_status": {
            "type": "string",
            "enum": [
              "unpaid",
              "processing",
              "paid",
              "failed",
              "nothing_due"
            ],
            "description": "Collection on the saved card. `unpaid` until a charge starts — within an hour of the statement, once a card is saved. `failed` moves the account to Free when the month ends."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "StatementPage": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Statement"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "TopUpCreate": {
        "type": "object",
        "required": [
          "amount_usd"
        ],
        "properties": {
          "amount_usd": {
            "type": "number",
            "minimum": 5,
            "maximum": 999999.99,
            "description": "US dollars to buy, in whole cents.",
            "example": 50
          },
          "saved_card": {
            "type": "boolean",
            "default": false,
            "description": "Charge the saved card now instead of opening a checkout."
          }
        }
      },
      "TopUp": {
        "type": "object",
        "required": [
          "id",
          "amount_usd",
          "status",
          "checkout_url",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The payment's or the checkout's ID at the payment provider.",
            "example": "cs_test_a1b2c3"
          },
          "amount_usd": {
            "type": "number",
            "description": "What is credited once paid: charged in US dollars, or its equivalent in reais.",
            "example": 50
          },
          "status": {
            "type": "string",
            "enum": [
              "paid",
              "checkout"
            ],
            "description": "`paid`: the saved card was charged and the credit added. `checkout`: nothing is charged until the caller pays at `checkout_url`."
          },
          "checkout_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "The hosted page that takes the card; `null` when paid.",
            "example": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When an unpaid checkout lapses; `null` when paid."
          }
        }
      },
      "PaymentMethod": {
        "type": "object",
        "required": [
          "brand",
          "last4",
          "exp_month",
          "exp_year",
          "currency",
          "exchange_rate"
        ],
        "properties": {
          "brand": {
            "type": "string",
            "example": "visa"
          },
          "last4": {
            "type": "string",
            "example": "4242"
          },
          "exp_month": {
            "type": "integer",
            "example": 12
          },
          "exp_year": {
            "type": "integer",
            "example": 2030
          },
          "currency": {
            "type": "string",
            "enum": [
              "usd",
              "brl"
            ],
            "description": "What the card is charged in, from its issuing country.",
            "example": "usd"
          },
          "exchange_rate": {
            "type": "number",
            "nullable": true,
            "description": "Units of `currency` per US dollar for a charge made now: 1 for `usd`, the Banco Central's latest closing PTAX selling rate for `brl`. `null` while that rate cannot be read.",
            "example": 1
          }
        }
      },
      "PaymentMethodSetup": {
        "type": "object",
        "required": [
          "id",
          "checkout_url",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The setup page's ID at the payment provider.",
            "example": "cs_test_a1b2c3"
          },
          "checkout_url": {
            "type": "string",
            "format": "uri",
            "description": "The hosted page that takes the card.",
            "example": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When an unused page lapses."
          }
        }
      },
      "AutoRecharge": {
        "type": "object",
        "required": [
          "enabled",
          "amount_usd",
          "below_usd",
          "last_attempted_at"
        ],
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "amount_usd": {
            "type": "number",
            "nullable": true,
            "description": "What each recharge buys. `null` while off.",
            "example": 20
          },
          "below_usd": {
            "type": "number",
            "nullable": true,
            "description": "The balance under which a recharge is charged. `null` while off.",
            "example": 5
          },
          "last_attempted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When a recharge was last charged, whatever its outcome."
          }
        }
      },
      "AutoRechargeUpdate": {
        "type": "object",
        "required": [
          "amount_usd",
          "below_usd"
        ],
        "properties": {
          "amount_usd": {
            "type": "number",
            "minimum": 5,
            "maximum": 999999.99,
            "description": "US dollars per recharge, in whole cents.",
            "example": 20
          },
          "below_usd": {
            "type": "number",
            "minimum": 0,
            "maximum": 999999.99,
            "description": "Recharge when the balance falls below this, in whole cents.",
            "example": 5
          }
        }
      },
      "UserPlanChangeRequest": {
        "type": "object",
        "required": [
          "plan"
        ],
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "business"
            ]
          }
        }
      },
      "UserPlanQuote": {
        "type": "object",
        "required": [
          "plan",
          "charge_usd"
        ],
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "business"
            ]
          },
          "charge_usd": {
            "type": "number",
            "description": "What `PUT /v1/users/me/plan` would charge now, before any conversion to reais; 0 when nothing.",
            "example": 24.5
          }
        }
      },
      "UserPlanChange": {
        "type": "object",
        "required": [
          "plan",
          "next_plan",
          "charged_usd",
          "checkout_url",
          "expires_at"
        ],
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "business",
              "enterprise"
            ],
            "description": "The plan in effect."
          },
          "next_plan": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "business",
              "enterprise"
            ],
            "description": "The plan from next month."
          },
          "charged_usd": {
            "type": "number",
            "description": "What the saved card was charged now; 0 when nothing was.",
            "example": 24.5
          },
          "checkout_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "The hosted page the upgrade waits on, which charges it and saves the card; `null` when nothing waits.",
            "example": null
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When an unpaid `checkout_url` lapses.",
            "example": null
          }
        }
      },
      "EventType": {
        "type": "string",
        "description": "A naturali event type. The set below is what this API emits — deliberately only the events it can itself cause, so no declared name is one that never fires. It grows as modules land.\n",
        "enum": [
          "conversation.started",
          "message.received",
          "generation.completed",
          "generation.failed",
          "usage.threshold_crossed",
          "project.paused",
          "project.resumed",
          "decision.completed",
          "decision.failed"
        ],
        "example": "conversation.started"
      },
      "EventSubscription": {
        "type": "array",
        "minItems": 1,
        "description": "Which events this endpoint receives. Each entry is an exact type (`generation.completed`), a resource wildcard (`generation.*`), or `*` for everything. A bare resource name (`generation`) matches nothing and is rejected.\n",
        "items": {
          "type": "string"
        },
        "example": [
          "generation.*",
          "message.received"
        ]
      },
      "Event": {
        "type": "object",
        "description": "The envelope POSTed to your endpoint. It is also what a delivery's `payload` holds, byte for byte, so the signed body and the recorded one are the same document.\n",
        "required": [
          "id",
          "type",
          "project_id",
          "resource_type",
          "resource_id",
          "data",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique per event (`evt_` prefix), and stable across redeliveries — dedupe on this if your receiver must process an event exactly once.\n",
            "example": "evt_V1StGXR8Z5jdHi6B"
          },
          "type": {
            "$ref": "#/components/schemas/EventType"
          },
          "project_id": {
            "type": "string",
            "x-naturali-ref": "project",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "resource_type": {
            "type": "string",
            "description": "What the event is about.",
            "enum": [
              "conversation",
              "message",
              "generation",
              "usage_threshold",
              "project",
              "decision"
            ],
            "example": "conversation"
          },
          "resource_id": {
            "type": "string",
            "description": "The id of that resource, in the form its own API uses.",
            "example": "conv_V1StGXR8Z5jdHi6B"
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "The resource, shaped exactly as its own API returns it — a `generation.completed` payload carries the same object `getGeneration` would.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-31T00:00:00.000Z"
          }
        }
      },
      "Webhook": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "url",
          "events",
          "description",
          "active",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Public webhook ID (whk_ prefix).",
            "example": "whk_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "x-naturali-ref": "project",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Where deliveries are POSTed.",
            "example": "https://example.com/hooks/naturali"
          },
          "events": {
            "$ref": "#/components/schemas/EventSubscription"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Operator-facing label.",
            "example": "billing service"
          },
          "active": {
            "type": "boolean",
            "description": "Whether deliveries are attempted.",
            "example": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-31T00:00:00.000Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-31T00:00:00.000Z"
          }
        }
      },
      "WebhookWithSecret": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Webhook"
          },
          {
            "type": "object",
            "required": [
              "secret"
            ],
            "properties": {
              "secret": {
                "type": "string",
                "description": "The signing key (`whsec_` prefix), returned only by create and rotate. Never readable afterwards.\n",
                "example": "whsec_V1StGXR8Z5jdHi6BQe4kL2mN"
              }
            }
          }
        ]
      },
      "WebhookCreate": {
        "type": "object",
        "required": [
          "url",
          "events"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "An `https://` endpoint (`http://` is accepted for localhost, so a tunnel works in development).\n",
            "example": "https://example.com/hooks/naturali"
          },
          "events": {
            "$ref": "#/components/schemas/EventSubscription"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "billing service"
          },
          "active": {
            "type": "boolean",
            "default": true,
            "example": true
          }
        }
      },
      "WebhookUpdate": {
        "type": "object",
        "description": "At least one field is required.",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "example": "https://example.com/hooks/naturali-v2"
          },
          "events": {
            "$ref": "#/components/schemas/EventSubscription"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "billing service (staging)"
          },
          "active": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "WebhookList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Webhook"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Cursor for the next page, or null when there are no more.",
            "example": null
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "description": "One event addressed to one endpoint, and everything that happened to it.",
        "required": [
          "id",
          "project_id",
          "webhook_id",
          "event_id",
          "event_type",
          "payload",
          "status",
          "status_code",
          "attempts",
          "next_attempt_at",
          "last_attempt_at",
          "response_body",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Public delivery ID (whd_ prefix). Sent with the request as `X-Naturali-Delivery`.\n",
            "example": "whd_V1StGXR8Z5jdHi6B"
          },
          "project_id": {
            "type": "string",
            "x-naturali-ref": "project",
            "example": "proj_V1StGXR8Z5jdHi6B"
          },
          "webhook_id": {
            "type": "string",
            "x-naturali-ref": "webhook",
            "example": "whk_V1StGXR8Z5jdHi6B"
          },
          "event_id": {
            "type": "string",
            "description": "The event's id; shared by every delivery and redelivery of it.",
            "example": "evt_V1StGXR8Z5jdHi6B"
          },
          "event_type": {
            "$ref": "#/components/schemas/EventType"
          },
          "payload": {
            "$ref": "#/components/schemas/Event"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "success",
              "failed"
            ],
            "description": "`pending` until a 2xx is received (`success`) or the attempts are exhausted (`failed`). A failed delivery can be replayed with `…:redeliver`.\n",
            "example": "success"
          },
          "status_code": {
            "type": "integer",
            "nullable": true,
            "description": "The receiver's HTTP status on the last attempt; null when the attempt never got a response (DNS, TLS, refused, timed out).\n",
            "example": 200
          },
          "attempts": {
            "type": "integer",
            "description": "Attempts made so far.",
            "example": 1
          },
          "next_attempt_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the next retry is due; null once the delivery is terminal.",
            "example": null
          },
          "last_attempt_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-07-31T00:00:01.000Z"
          },
          "response_body": {
            "type": "string",
            "nullable": true,
            "description": "A truncated snippet of the receiver's response, or the transport error when there was no response.\n",
            "example": "ok"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-31T00:00:00.000Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-31T00:00:01.000Z"
          }
        }
      },
      "WebhookDeliveryList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookDelivery"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Cursor for the next page, or null when there are no more.",
            "example": null
          }
        }
      },
      "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-naturali-ref": "agents",
                    "type": "string",
                    "nullable": true,
                    "description": "The agent a `kind: agent` dispatch generates with."
                  },
                  "orchestration_id": {
                    "x-naturali-ref": "orchestrations",
                    "type": "string",
                    "nullable": true,
                    "description": "The orchestration a `kind: orchestration` dispatch starts a run of."
                  },
                  "tool_id": {
                    "x-naturali-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": {
          "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-naturali-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 the runtime reflects the workflow surface **of its own time**, so it may carry fields the current API no longer documents.",
            "properties": {
              "states": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/WorkflowState"
                }
              },
              "transitions": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/WorkflowTransition"
                }
              },
              "payload_schema": {
                "type": "object",
                "nullable": true
              }
            }
          },
          "label": {
            "type": "string",
            "nullable": true,
            "description": "Optional human tag for this version, e.g. `pre-rewire`. Set from the `version_label` field of a write, the `label` field of a restore, or generated for one.",
            "example": "restored from v2"
          },
          "created_by": {
            "x-naturali-ref": "users",
            "type": "string",
            "nullable": true,
            "description": "Public ID of the user whose action produced this version. Null for writes with no request user behind them."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RestoreWorkflowVersionRequest": {
        "type": "object",
        "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"
          }
        }
      }
    },
    "parameters": {
      "ProjectId": {
        "name": "project_id",
        "in": "path",
        "required": true,
        "description": "Project public ID (proj_ prefix).",
        "schema": {
          "type": "string",
          "example": "proj_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"
        ]
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum items per page — an integer from 1 to 100 (default 20).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Opaque pagination cursor from a previous response's next_cursor.",
        "schema": {
          "type": "string"
        }
      },
      "Identifier": {
        "name": "identifier",
        "in": "path",
        "required": true,
        "description": "The prefixed, self-describing channel identifier (`whatsapp:dm:<phone>`, `discord:dm:<user_id>`, `discord:thread:<guild_id>:<channel_id>`), URL-encoded.\n",
        "schema": {
          "type": "string",
          "example": "instagram:dm:17841400000000000"
        }
      },
      "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
        }
      },
      "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"
      },
      "ApiKeyId": {
        "name": "api_key_id",
        "in": "path",
        "required": true,
        "description": "API key public ID (key_ prefix).",
        "schema": {
          "type": "string",
          "example": "key_V1StGXR8Z5jdHi6B"
        }
      },
      "approval_id": {
        "name": "approval_id",
        "in": "path",
        "required": true,
        "description": "Approval item ID",
        "schema": {
          "type": "string",
          "example": "apr_V1StGXR8Z5jdHi6B"
        }
      },
      "GrantId": {
        "name": "grant_id",
        "in": "path",
        "required": true,
        "description": "Grant public ID (agr_ prefix).",
        "schema": {
          "type": "string",
          "example": "agr_V1StGXR8Z5jdHi6B"
        }
      },
      "LinkToken": {
        "name": "token",
        "in": "query",
        "required": true,
        "description": "The single-use token from the link the Assistant sent on the channel.",
        "schema": {
          "type": "string",
          "example": "alt_9f8e7d6c5b4a39281706"
        }
      },
      "chain_id": {
        "name": "chain_id",
        "in": "path",
        "required": true,
        "description": "Continuation chain ID",
        "schema": {
          "type": "string",
          "example": "chain_V1StGXR8Z5jdHi6B"
        }
      },
      "ChannelId": {
        "name": "channel_id",
        "in": "path",
        "required": true,
        "description": "Channel public ID (chan_ prefix).",
        "schema": {
          "type": "string",
          "example": "chan_V1StGXR8Z5jdHi6B"
        }
      },
      "RouteId": {
        "name": "route_id",
        "in": "path",
        "required": true,
        "description": "Route public ID (route_ prefix).",
        "schema": {
          "type": "string",
          "example": "route_V1StGXR8Z5jdHi6B"
        }
      },
      "ConversationId": {
        "name": "conversation_id",
        "in": "path",
        "required": true,
        "description": "Conversation public ID (conv_ prefix).",
        "schema": {
          "type": "string",
          "example": "conv_V1StGXR8Z5jdHi6B"
        }
      },
      "MessagesLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum messages per page.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 50
        }
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "required": false,
        "description": "Number of messages to skip.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      },
      "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
        }
      },
      "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}}"
      },
      "exception_id": {
        "name": "exception_id",
        "in": "path",
        "required": true,
        "description": "Exception item ID",
        "schema": {
          "type": "string",
          "example": "exc_V1StGXR8Z5jdHi6B"
        }
      },
      "InstallId": {
        "name": "install_id",
        "in": "path",
        "required": true,
        "description": "Install public ID (ins_ prefix).",
        "schema": {
          "type": "string",
          "example": "ins_V1StGXR8Z5jdHi6B"
        }
      },
      "ListingId": {
        "name": "listing_id",
        "in": "path",
        "required": true,
        "description": "Listing public ID (lst_ prefix).",
        "schema": {
          "type": "string",
          "example": "lst_V1StGXR8Z5jdHi6B"
        }
      },
      "ModelName": {
        "name": "model",
        "in": "path",
        "required": true,
        "description": "The model's name, as `GET /v1/models` returns it.",
        "schema": {
          "type": "string",
          "example": "nova-lite-v1"
        }
      },
      "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_...)"
      },
      "UsageMeterType": {
        "name": "meter_type",
        "in": "query",
        "required": false,
        "description": "Narrow the rollup to one meter type (llm_tokens, compute_execution, api_request, storage, tool_execution). Omit to include every meter. Useful with group_by=model, whose dimension otherwise mixes model ids with platform SKUs. An unrecognized value yields an empty rollup, not an error.\n",
        "schema": {
          "type": "string",
          "example": "llm_tokens"
        }
      },
      "UsageSessionId": {
        "name": "session_id",
        "in": "query",
        "required": false,
        "description": "Narrow the whole rollup — every bucket and the top-level totals alike — to one session's traffic. Combines with group_by: group_by=day&session_id=... is one conversation's spend per day, group_by=model the same spend split by model. This is the figure behind the usage roll-up a single session read carries, broken down. An id naming no session in this project yields an empty rollup, never the project total.\n",
        "schema": {
          "type": "string",
          "example": "sess_V1StGXR8Z5jdHi6B"
        }
      },
      "UsageActorId": {
        "name": "actor_id",
        "in": "query",
        "required": false,
        "description": "The same narrowing, for one end user across every session they appear in — what a customer re-billing their own users charges each of them for. An id naming no actor in this project yields an empty rollup, never the project total.\n",
        "schema": {
          "type": "string",
          "example": "actor_V1StGXR8Z5jdHi6B"
        }
      },
      "UsageAgentId": {
        "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",
          "example": "agent_V1StGXR8Z5jdHi6B"
        }
      },
      "UsageAiProviderId": {
        "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",
          "example": "aip_V1StGXR8Z5jdHi6B"
        }
      },
      "UsageOrchestrationRunId": {
        "name": "orchestration_run_id",
        "in": "query",
        "required": false,
        "description": "Narrow to the events one orchestration run metered.",
        "schema": {
          "type": "string",
          "example": "orun_V1StGXR8Z5jdHi6B"
        }
      },
      "UsageOrchestrationId": {
        "name": "orchestration_id",
        "in": "query",
        "required": false,
        "description": "Narrow to the runs of one orchestration — the ones 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.\n",
        "schema": {
          "type": "string",
          "example": "orch_V1StGXR8Z5jdHi6B"
        }
      },
      "UsageGenerationId": {
        "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, such as tokens and compute.\n",
        "schema": {
          "type": "string",
          "example": "gen_V1StGXR8Z5jdHi6B"
        }
      },
      "UsageTraceId": {
        "name": "trace_id",
        "in": "query",
        "required": false,
        "description": "Narrow to the events recorded under one trace.",
        "schema": {
          "type": "string",
          "example": "trace_V1StGXR8Z5jdHi6B"
        }
      },
      "UsageSource": {
        "name": "source",
        "in": "query",
        "required": false,
        "description": "Narrow by what the spend was incurred for — eval and eval_judge are verification spend. Ordinary traffic carries no source, so it cannot be selected by this filter; omit the filter for everything.\n",
        "schema": {
          "type": "string",
          "example": "eval"
        }
      },
      "UsageTriggerId": {
        "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",
          "example": "trig_V1StGXR8Z5jdHi6B"
        }
      },
      "UsageActionId": {
        "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"
        }
      },
      "Force": {
        "name": "force",
        "in": "query",
        "required": false,
        "description": "When true, delete the resource together with its dependents instead of returning 409. Destructive and irreversible.\n",
        "schema": {
          "type": "boolean",
          "default": false
        }
      },
      "SessionId": {
        "name": "session_id",
        "in": "path",
        "required": true,
        "description": "Session public ID",
        "schema": {
          "type": "string",
          "example": "sess_V1StGXR8Z5jdHi6B"
        }
      },
      "stop_id": {
        "name": "stop_id",
        "in": "path",
        "required": true,
        "description": "The stop's ID (sst_ prefix).",
        "schema": {
          "type": "string"
        }
      },
      "WebhookId": {
        "name": "webhook_id",
        "in": "path",
        "required": true,
        "description": "Webhook public ID (whk_ prefix).",
        "schema": {
          "type": "string",
          "example": "whk_V1StGXR8Z5jdHi6B"
        }
      },
      "DeliveryId": {
        "name": "delivery_id",
        "in": "path",
        "required": true,
        "description": "Delivery public ID (whd_ prefix).",
        "schema": {
          "type": "string",
          "example": "whd_V1StGXR8Z5jdHi6B"
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A naturali API key (nat_sk_…) or a session JWT."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "A connected app's OAuth access token, issued by this API's authorization server (discovery: /.well-known/oauth-authorization-server). Its one scope carries every operation, confined to the projects the user chose when approving the app.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://api.naturali.ai/authorize",
            "tokenUrl": "https://api.naturali.ai/token",
            "refreshUrl": "https://api.naturali.ai/token",
            "scopes": {
              "mcp:access": "Every operation this API serves, on the projects the grant covers."
            }
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid credentials.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "BadRequest": {
        "description": "The request was malformed or failed validation.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The credential is scoped to a different project, or the caller's role in the project does not carry this action.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "NotFound": {
        "description": "The resource does not exist (existence is not leaked).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "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"
            }
          }
        }
      },
      "ApiKeyForbidden": {
        "description": "Authenticated, but not permitted to act on this key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "ConversationForbidden": {
        "description": "The credential cannot hold the account's conversation (`access_denied`): it is confined to one project, or it is the Assistant's own turn credential.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "TurnUnavailable": {
        "description": "The turn could not be run or read (`upstream_unavailable`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "AssistantUnavailable": {
        "description": "This deployment serves no Assistant conversation (`assistant_unavailable`).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Conflict": {
        "description": "The request conflicts with the resource's current state.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "NotImplemented": {
        "description": "The requested path is not enabled on this deployment (e.g. embedded signup before Meta App credentials are configured, or a Discord channel before `CHANNEL_TOKEN_KEY` is set).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "UpstreamUnavailable": {
        "description": "The upstream runtime, or Meta, could not complete the operation.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "InstallConflict": {
        "description": "`listing_suspended` (the listing cannot be installed while suspended) or `install_in_use` (resources still name the installed resource; `details.references` lists them).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "ListingConflict": {
        "description": "`listing_exists` (the resource already has a listing) or `invalid_listing_state` (the move is not allowed from the listing's current state).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "ListingModelNotPriced": {
        "description": "`model_not_priced`: the agent runs on a naturali model that has no price yet, so its use could not be billed.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "StopNotFound": {
        "description": "No such stop on this account, or it was already restored or dismissed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "StopNotRestorable": {
        "description": "The stop cannot be restored now.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "StopRestoreUnavailable": {
        "description": "The runtime could not turn the item back on; the stop stays listed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "WebhookLimitReached": {
        "description": "The project already holds 20 webhooks (`webhook_limit_reached`); `details.limit` carries the ceiling.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "SealingKeyNotConfigured": {
        "description": "The requested path is not enabled on this deployment — a webhook secret cannot be stored where no credential-sealing key is configured.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    }
  }
}
