openapi: 3.1.0
info:
  title: MonkAI Trace API
  version: "0.5.0"
  summary: Language-agnostic REST API for capturing AI agent traces, sessions, logs, and conversation records.
  description: |
    The MonkAI Trace REST API exposes the same backend used by the official
    Python SDK (`monkai-trace`) so that any runtime (Node.js, Go, Java, .NET,
    Rust, etc.) can send traces without depending on the SDK.

    The API has three logical surfaces:

    1. **Sessions** — track a logical conversation across multiple traces.
    2. **Traces** — fine-grained events (LLM call, tool call, agent handoff, log).
       Recommended for non-Python integrations.
    3. **Records / Logs** — bulk endpoints used by the Python SDK and for batch
       imports of historical data. Useful when an integration already produces
       full conversation records.

    All endpoints accept JSON, return JSON, and require a tracer token. See
    [`docs/http_rest_api.md`](./http_rest_api.md) for narrative documentation.
  contact:
    name: MonkAI
    url: https://monkai.ai
  license:
    name: MIT
    url: https://github.com/BeMonkAI/monkai-trace/blob/main/LICENSE

servers:
  - url: https://api.monkai.com.br/trace/v1
    description: Production — recommended for all clients (custom domain via Vercel Edge proxy)
  - url: https://lpvbvnqrozlwalnkvrgk.supabase.co/functions/v1/monkai-api/v1
    description: Production — direct Supabase URL (still works; prefer the custom domain above)
  - url: https://lpvbvnqrozlwalnkvrgk.supabase.co/functions/v1/monkai-api
    description: Production — legacy unversioned Supabase URL (still supported)

tags:
  - name: Health
    description: Liveness check (no auth required).
  - name: Sessions
    description: Manage conversation sessions.
  - name: Traces
    description: Record fine-grained events. Preferred for HTTP clients in any language.
  - name: Records
    description: Conversation records (used by the Python SDK and for batch imports).
  - name: Logs
    description: Operational log entries.
  - name: Query
    description: Read records and logs.
  - name: Export
    description: Bulk export with server-side pagination.

security:
  - bearerAuth: []
  - tracerTokenHeader: []

paths:
  # ============================================================
  # HEALTH (no auth)
  # ============================================================
  /health:
    get:
      tags: [Health]
      summary: Liveness check
      description: |
        Cheap, unauthenticated liveness probe. Returns `200` with a small
        JSON body when the edge function is responsive. Use it for
        monitors, uptime checks, and post-deploy smoke tests. Also
        supports `HEAD` (RFC 7231) — returns the same headers without
        a body so HEAD-based monitors don't waste bandwidth.
      operationId: healthCheck
      security: []
      responses:
        "200":
          description: Service is responsive
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "500":
          $ref: "#/components/responses/InternalError"
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl "https://api.monkai.com.br/trace/v1/health"
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies, no auth required
            const res = await fetch("https://api.monkai.com.br/trace/v1/health", { method: "GET" });
            console.log(res.status, res.headers.get("x-request-id"));
        - lang: Python
          label: Python
          source: |
            import requests
            r = requests.get("https://api.monkai.com.br/trace/v1/health")
            print(r.status_code, r.headers["x-request-id"])
    head:
      tags: [Health]
      summary: Liveness check (HEAD variant)
      description: |
        Same semantics as `GET /health` but returns no body. Useful for
        bandwidth-sensitive monitors.
      operationId: healthCheckHead
      security: []
      responses:
        "200":
          description: Service is responsive (no body)
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
        "500":
          $ref: "#/components/responses/InternalError"

  # ============================================================
  # SESSIONS
  # ============================================================
  /sessions/create:
    post:
      tags: [Sessions]
      summary: Create a new session
      description: |
        Creates a new session for tracking a conversation flow. Use this when
        you want to force a fresh session, regardless of recent activity.
      operationId: createSession
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionCreateRequest"
            examples:
              whatsapp:
                summary: WhatsApp user
                value:
                  namespace: my-agent
                  user_id: "5521999998888"
                  inactivity_timeout: 300
                  metadata:
                    platform: whatsapp
      responses:
        "200":
          description: Session created
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Session"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/sessions/create" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"namespace":"my-agent","user_id":"5521999998888","inactivity_timeout":300}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/sessions/create", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"namespace":"my-agent","user_id":"5521999998888","inactivity_timeout":300})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/sessions/create",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"namespace":"my-agent","user_id":"5521999998888","inactivity_timeout":300},
            )
            print(r.headers["x-request-id"], r.json())
  /sessions/get-or-create:
    post:
      tags: [Sessions]
      summary: Get an active session or create a new one
      description: |
        Returns an existing active session for `(namespace, user_id)` if there
        was activity within `inactivity_timeout` seconds; otherwise creates a
        new one. This is the endpoint used by the Python SDK to keep session
        continuity across stateless environments (REST APIs, serverless).
      operationId: getOrCreateSession
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionGetOrCreateRequest"
      responses:
        "200":
          description: Session returned (existing or new)
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionGetOrCreateResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

  # ============================================================
  # TRACES (language-agnostic surface)
  # ============================================================
        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/sessions/get-or-create" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"namespace":"my-agent","user_id":"5521999998888"}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/sessions/get-or-create", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"namespace":"my-agent","user_id":"5521999998888"})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/sessions/get-or-create",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"namespace":"my-agent","user_id":"5521999998888"},
            )
            print(r.headers["x-request-id"], r.json())
  /traces/llm:
    post:
      tags: [Traces]
      summary: Trace an LLM call
      operationId: traceLlmCall
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
        - &id001
          $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LlmTraceRequest"
      responses:
        "200":
          description: Trace recorded
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            Idempotency-Replay:
              $ref: '#/components/headers/IdempotencyReplay'
            Idempotency-Original-Request-ID:
              $ref: '#/components/headers/IdempotencyOriginalRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LlmTraceResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

        '422':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/traces/llm" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"session_id":"sess_abc","model":"gpt-4","input":{"messages":[{"role":"user","content":"Hi"}]},"output":{"content":"Hello"}}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/traces/llm", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"session_id":"sess_abc","model":"gpt-4","input":{"messages":[{"role":"user","content":"Hi"}]},"output":{"content":"Hello"}})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/traces/llm",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"session_id":"sess_abc","model":"gpt-4","input":{"messages":[{"role":"user","content":"Hi"}]},"output":{"content":"Hello"}},
            )
            print(r.headers["x-request-id"], r.json())
  /traces/tool:
    post:
      tags: [Traces]
      summary: Trace a tool/function call
      operationId: traceToolCall
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
        - *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ToolTraceRequest"
      responses:
        "200":
          description: Trace recorded
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            Idempotency-Replay:
              $ref: '#/components/headers/IdempotencyReplay'
            Idempotency-Original-Request-ID:
              $ref: '#/components/headers/IdempotencyOriginalRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ToolTraceResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

        '422':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/traces/tool" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"session_id":"sess_abc","tool_name":"get_weather","arguments":{"city":"SP"},"result":{"temp":24}}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/traces/tool", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"session_id":"sess_abc","tool_name":"get_weather","arguments":{"city":"SP"},"result":{"temp":24}})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/traces/tool",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"session_id":"sess_abc","tool_name":"get_weather","arguments":{"city":"SP"},"result":{"temp":24}},
            )
            print(r.headers["x-request-id"], r.json())
  /traces/handoff:
    post:
      tags: [Traces]
      summary: Trace an agent-to-agent handoff
      operationId: traceHandoff
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
        - *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HandoffTraceRequest"
      responses:
        "200":
          description: Trace recorded
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            Idempotency-Replay:
              $ref: '#/components/headers/IdempotencyReplay'
            Idempotency-Original-Request-ID:
              $ref: '#/components/headers/IdempotencyOriginalRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HandoffTraceResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

        '422':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/traces/handoff" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"session_id":"sess_abc","from_agent":"triage","to_agent":"sales","reason":"intent matched"}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/traces/handoff", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"session_id":"sess_abc","from_agent":"triage","to_agent":"sales","reason":"intent matched"})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/traces/handoff",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"session_id":"sess_abc","from_agent":"triage","to_agent":"sales","reason":"intent matched"},
            )
            print(r.headers["x-request-id"], r.json())
  /traces/log:
    post:
      tags: [Traces]
      summary: Trace a log entry
      description: |
        Records a log entry. Either `session_id` or `namespace` must be provided.
      operationId: traceLog
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
        - *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LogTraceRequest"
      responses:
        "200":
          description: Log recorded
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            Idempotency-Replay:
              $ref: '#/components/headers/IdempotencyReplay'
            Idempotency-Original-Request-ID:
              $ref: '#/components/headers/IdempotencyOriginalRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LogTraceResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

  # ============================================================
  # RECORDS (bulk — used by Python SDK)
  # ============================================================
        '422':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/traces/log" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"session_id":"sess_abc","level":"info","message":"step 5 done"}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/traces/log", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"session_id":"sess_abc","level":"info","message":"step 5 done"})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/traces/log",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"session_id":"sess_abc","level":"info","message":"step 5 done"},
            )
            print(r.headers["x-request-id"], r.json())
  /traces/batch:
    post:
      tags: [Traces]
      summary: Batch trace endpoint (mixed types)
      description: |
        Submit up to 100 traces in a single request. Each item carries a
        `type` field (`llm` | `tool` | `handoff` | `log`) and the same
        body shape as the per-type endpoint, minus the `type` field.

        **Partial success is allowed.** When the outer envelope is
        well-formed, the response is always `200`; per-item status is
        reported in the `results` array (`status: "ok"` or
        `status: "error"`). The outer `success` is `true` only when
        every item succeeded.

        Use this whenever a single client interaction produces multiple
        traces (an LLM call followed by a tool call, plus a log entry,
        for example) — it cuts N round-trips down to one.
      operationId: traceBatch
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
        - *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchTraceRequest"
      responses:
        "200":
          description: |
            Batch processed. Inspect `results` for per-item status.
            `failed > 0` means at least one item errored — the others
            were still recorded.
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            Idempotency-Replay:
              $ref: '#/components/headers/IdempotencyReplay'
            Idempotency-Original-Request-ID:
              $ref: '#/components/headers/IdempotencyOriginalRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchTraceResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

        '422':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/traces/batch" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"traces":[{"type":"llm","session_id":"sess_abc","model":"gpt-4","input":{"messages":[{"role":"user","content":"hi"}]},"output":{"content":"hello"}},{"type":"tool","session_id":"sess_abc","tool_name":"get_weather","arguments":{"city":"SP"},"result":{"temp":24}},{"type":"log","session_id":"sess_abc","level":"info","message":"done"}]}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/traces/batch", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"traces":[{"type":"llm","session_id":"sess_abc","model":"gpt-4","input":{"messages":[{"role":"user","content":"hi"}]},"output":{"content":"hello"}},{"type":"tool","session_id":"sess_abc","tool_name":"get_weather","arguments":{"city":"SP"},"result":{"temp":24}},{"type":"log","session_id":"sess_abc","level":"info","message":"done"}]})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/traces/batch",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"traces":[{"type":"llm","session_id":"sess_abc","model":"gpt-4","input":{"messages":[{"role":"user","content":"hi"}]},"output":{"content":"hello"}},{"type":"tool","session_id":"sess_abc","tool_name":"get_weather","arguments":{"city":"SP"},"result":{"temp":24}},{"type":"log","session_id":"sess_abc","level":"info","message":"done"}]},
            )
            print(r.headers["x-request-id"], r.json())
  /records/upload:
    post:
      tags: [Records]
      summary: Upload conversation records (batch)
      description: |
        Batch upload of conversation records. Used by the Python SDK
        (`MonkAIClient.upload_record` / `upload_records_batch`). Server-side
        deduplication is applied; the response indicates how many were inserted
        vs. dropped as duplicates.
      operationId: uploadRecords
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RecordsUploadRequest"
      responses:
        "200":
          description: Records processed
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecordsUploadResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/records/upload" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"records":[{"namespace":"my-agent","agent":"bot","msg":[{"role":"user","content":"hi"}]}]}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/records/upload", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"records":[{"namespace":"my-agent","agent":"bot","msg":[{"role":"user","content":"hi"}]}]})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/records/upload",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"records":[{"namespace":"my-agent","agent":"bot","msg":[{"role":"user","content":"hi"}]}]},
            )
            print(r.headers["x-request-id"], r.json())
  /logs/upload:
    post:
      tags: [Logs]
      summary: Upload operational logs (batch)
      description: Batch upload of structured log entries.
      operationId: uploadLogs
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LogsUploadRequest"
      responses:
        "200":
          description: Logs processed
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LogsUploadResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

  # ============================================================
  # QUERY
  # ============================================================
        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/logs/upload" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"logs":[{"namespace":"my-agent","level":"info","message":"hi"}]}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/logs/upload", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"logs":[{"namespace":"my-agent","level":"info","message":"hi"}]})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/logs/upload",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"logs":[{"namespace":"my-agent","level":"info","message":"hi"}]},
            )
            print(r.headers["x-request-id"], r.json())
  /record_query:
    post:
      tags: [Query]
      summary: Query conversation records
      operationId: queryRecords
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RecordQueryRequest"
      responses:
        "200":
          description: Records matching the query
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecordQueryResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/record_query" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"namespace":"my-agent","query":{"limit":50}}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/record_query", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"namespace":"my-agent","query":{"limit":50}})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/record_query",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"namespace":"my-agent","query":{"limit":50}},
            )
            print(r.headers["x-request-id"], r.json())
  /logs/query:
    post:
      tags: [Query]
      summary: Query log entries
      operationId: queryLogs
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LogQueryRequest"
      responses:
        "200":
          description: Logs matching the query
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LogQueryResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

  # ============================================================
  # EXPORT
  # ============================================================
        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/logs/query" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"namespace":"my-agent","level":"error","limit":100}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/logs/query", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"namespace":"my-agent","level":"error","limit":100})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/logs/query",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"namespace":"my-agent","level":"error","limit":100},
            )
            print(r.headers["x-request-id"], r.json())
  /records/export:
    post:
      tags: [Export]
      summary: Export conversation records (JSON or CSV)
      operationId: exportRecords
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RecordExportRequest"
      responses:
        "200":
          description: Records exported
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecordExportResponse"
            text/csv:
              schema:
                type: string
                description: CSV content (when `format=csv`)
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/records/export" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"namespace":"my-agent","format":"json"}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/records/export", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"namespace":"my-agent","format":"json"})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/records/export",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"namespace":"my-agent","format":"json"},
            )
            print(r.headers["x-request-id"], r.json())
  /logs/export:
    post:
      tags: [Export]
      summary: Export logs (JSON or CSV)
      operationId: exportLogs
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LogExportRequest"
      responses:
        "200":
          description: Logs exported
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LogExportResponse"
            text/csv:
              schema:
                type: string
                description: CSV content (when `format=csv`)
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"

        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.monkai.com.br/trace/v1/logs/export" \
              -H "Authorization: Bearer $MONKAI_TRACER_TOKEN" \
              -H "Content-Type: application/json" \
              -H "X-Request-ID: $(uuidgen)" \
              -d '{"namespace":"my-agent","format":"csv"}'
        - lang: JavaScript
          label: Node.js
          source: |
            // Node.js 18+ — no dependencies
            const res = await fetch("https://api.monkai.com.br/trace/v1/logs/export", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.MONKAI_TRACER_TOKEN}`,
                "Content-Type": "application/json"
              },
              body: JSON.stringify({"namespace":"my-agent","format":"csv"})
            });
            console.log(res.headers.get("x-request-id"), await res.json());
        - lang: Python
          label: Python
          source: |
            import os, requests
            r = requests.post(
                "https://api.monkai.com.br/trace/v1/logs/export",
                headers={"Authorization": f"Bearer {os.environ['MONKAI_TRACER_TOKEN']}"},
                json={"namespace":"my-agent","format":"csv"},
            )
            print(r.headers["x-request-id"], r.json())
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: |
        Recommended authentication scheme. Send the tracer token as
        `Authorization: Bearer tk_<hex>`. RFC 6750 — works out of the
        box with `curl`, `fetch`, generated clients, and most API
        gateways.
    tracerTokenHeader:
      type: apiKey
      in: header
      name: tracer_token
      description: |
        Legacy authentication scheme, still supported. Send the tracer
        token in the `tracer_token` HTTP header. Both schemes accept
        the same `tk_<hex>` token and can be used interchangeably.
        New integrations should prefer `bearerAuth`.

  parameters:
    RequestIdHeader:
      name: X-Request-ID
      in: header
      required: false
      schema:
        type: string
        maxLength: 128
        pattern: '^[\x21-\x7E][\x20-\x7E]{0,127}$'
      description: |
        Optional client-supplied request correlation ID. If provided,
        the server preserves the value in the response `X-Request-ID`
        header (round-trip). If omitted, the server generates a UUIDv4.
        Useful for distributed traces and support correlation.

    IdempotencyKeyHeader:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 128
        pattern: '^[\x21-\x7E][\x20-\x7E]{0,127}$'
      description: |
        Optional client-supplied idempotency token (Phase 3). When
        present, the server caches the response under
        `(tenant, key)` for **24 hours**:

        - Same key + identical body → cached replay; the response
          is returned without re-running side effects (DB inserts,
          token charges). Replays carry `Idempotency-Replay: true`.
        - Same key + different body → `422 idempotency_key_conflict`.
        - Different (or missing) key → fresh execution, default
          behaviour.

        Typical pattern: generate a UUID per logical client operation
        and pass it on every retry of that operation. Errors are NOT
        cached, so retrying a failed call with the same key naturally
        re-executes.

        Supported on `/v1/traces/llm`, `/v1/traces/tool`,
        `/v1/traces/handoff`, `/v1/traces/log`, and
        `/v1/traces/batch`.

  headers:
    XRequestId:
      schema:
        type: string
      description: |
        Request correlation ID. Mirrors the value sent in the request
        `X-Request-ID` header when present, otherwise a server-generated
        UUIDv4. Always emitted on every response (200, 4xx, 5xx). Quote
        this value when reporting issues.

    IdempotencyReplay:
      schema:
        type: string
        enum: ["true"]
      description: |
        Present and set to `"true"` only when the response was served
        from the idempotency cache (same key + same body as a
        previous request within 24h). Absent on fresh executions.

    IdempotencyOriginalRequestId:
      schema:
        type: string
      description: |
        On a replay, mirrors the `X-Request-ID` of the **original**
        request whose result is being returned. Use it to find the
        first request in server logs.

    RateLimitLimit:
      schema:
        type: integer
        minimum: 0
      description: |
        Per-token request limit for the current bucket, in
        requests/minute. Bucket is determined by the route — see the
        narrative docs for the table. Emitted on every response from
        a rate-limited endpoint (200, 4xx, 5xx). The response also
        carries the modern `RateLimit-Limit` header (IETF draft) with
        the same value.

    RateLimitRemaining:
      schema:
        type: integer
        minimum: 0
      description: |
        Requests still available in the current 1-minute window.
        Reaches `0` on the request that exhausts the quota; the
        next call returns `429`. Also emitted as `RateLimit-Remaining`.

    RateLimitReset:
      schema:
        type: integer
        minimum: 0
        maximum: 60
      description: |
        Seconds until the current 1-minute window resets and the
        full quota is restored. Always between 0 and 60. Also
        emitted as `RateLimit-Reset`.

    RetryAfter:
      schema:
        type: integer
        minimum: 0
      description: |
        Seconds the client should wait before retrying. Emitted with
        `429 rate_limit_exceeded` (RFC 6585). Identical to
        `RateLimit-Reset` on `429` responses.

  responses:
    BadRequest:
      description: Bad Request — required field missing or invalid payload
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Unauthorized — invalid or missing tracer token
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Forbidden — token does not have access to the resource
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: |
        Conflict — typically `idempotency_key_conflict`, raised when an
        `Idempotency-Key` is reused with a different request body. Pick
        a new key or fix the body.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    TooManyRequests:
      description: |
        Per-token rate limit exceeded for the bucket the route belongs
        to. The error envelope carries `code: "rate_limit_exceeded"`.
        Wait `Retry-After` seconds (or `RateLimit-Reset` — they're
        identical here) and retry. Limits are per-token, per-minute,
        fixed window.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestId"
        X-RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    InternalError:
      description: Internal Server Error
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    # ---------- Common ----------
    HealthResponse:
      type: object
      required: [status, service, api_version, timestamp]
      properties:
        status:
          type: string
          enum: [ok]
          description: Always `ok` when the endpoint responds.
        service:
          type: string
          example: monkai-trace
        api_version:
          type: string
          description: |
            Detected URL version. `v1` for `/v1/health`; the legacy
            unversioned `/health` also reports `v1` (it acts as a v1
            alias).
          example: v1
        timestamp:
          type: string
          format: date-time
          description: Server-side timestamp at the moment of the response.

    BatchTraceItem:
      type: object
      required: [type]
      discriminator:
        propertyName: type
        mapping:
          llm: "#/components/schemas/BatchTraceItemLlm"
          tool: "#/components/schemas/BatchTraceItemTool"
          handoff: "#/components/schemas/BatchTraceItemHandoff"
          log: "#/components/schemas/BatchTraceItemLog"
      properties:
        type:
          type: string
          enum: [llm, tool, handoff, log]
          description: Selects the per-type body shape.
      oneOf:
        - $ref: "#/components/schemas/BatchTraceItemLlm"
        - $ref: "#/components/schemas/BatchTraceItemTool"
        - $ref: "#/components/schemas/BatchTraceItemHandoff"
        - $ref: "#/components/schemas/BatchTraceItemLog"

    BatchTraceItemLlm:
      allOf:
        - type: object
          required: [type]
          properties:
            type:
              type: string
              const: llm
        - $ref: "#/components/schemas/LlmTraceRequest"

    BatchTraceItemTool:
      allOf:
        - type: object
          required: [type]
          properties:
            type:
              type: string
              const: tool
        - $ref: "#/components/schemas/ToolTraceRequest"

    BatchTraceItemHandoff:
      allOf:
        - type: object
          required: [type]
          properties:
            type:
              type: string
              const: handoff
        - $ref: "#/components/schemas/HandoffTraceRequest"

    BatchTraceItemLog:
      allOf:
        - type: object
          required: [type]
          properties:
            type:
              type: string
              const: log
        - $ref: "#/components/schemas/LogTraceRequest"

    BatchTraceRequest:
      type: object
      required: [traces]
      properties:
        traces:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: "#/components/schemas/BatchTraceItem"

    BatchTraceResultItem:
      type: object
      required: [index, type, status]
      properties:
        index:
          type: integer
          minimum: 0
          description: Position in the request `traces` array.
        type:
          type: string
          enum: [llm, tool, handoff, log]
        status:
          type: string
          enum: [ok, error]
        result:
          description: |
            Present when `status === "ok"`. Shape mirrors the per-type
            response (`LlmTraceResponse`, `ToolTraceResponse`, etc.)
            minus the `success` flag.
          oneOf:
            - $ref: "#/components/schemas/LlmTraceResponse"
            - $ref: "#/components/schemas/ToolTraceResponse"
            - $ref: "#/components/schemas/HandoffTraceResponse"
            - $ref: "#/components/schemas/LogTraceResponse"
        error:
          description: |
            Present when `status === "error"`. Same shape as the
            error envelope, minus the wrapping `error` key.
          type: object
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string

    BatchTraceResponse:
      type: object
      required: [success, total, succeeded, failed, results]
      properties:
        success:
          type: boolean
          description: |
            `true` only when every item succeeded. Inspect `results`
            for per-item status.
        total:
          type: integer
          minimum: 0
        succeeded:
          type: integer
          minimum: 0
        failed:
          type: integer
          minimum: 0
        results:
          type: array
          items:
            $ref: "#/components/schemas/BatchTraceResultItem"

    Error:
      type: object
      description: |
        Structured error envelope (Phase 2). Every 4xx/5xx response
        from `/v1/` carries this shape. Some endpoints add extra context
        fields (e.g. `similar_namespaces`, `unregistered_namespaces`,
        `details`, `issues`) alongside the envelope — those are
        operation-specific and documented per response, but `error.code`
        is always populated and is the recommended branching key.
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code:
              type: string
              description: |
                Machine-readable error code. Stable across versions —
                clients should branch on this rather than substring-match
                `message`. New codes may be added; clients should treat
                unknown codes as the generic family code (the same first
                segment, e.g. `bad_request` for any 4xx the client
                doesn't recognise).
              enum:
                - bad_request
                - missing_field
                - invalid_payload
                - namespace_taken
                - namespace_too_similar
                - unauthorized
                - missing_token
                - invalid_token
                - token_expired
                - token_inactive
                - forbidden
                - not_found
                - internal_error
                - encryption_error
                - anonymization_error
                - idempotency_key_conflict
                - rate_limit_exceeded
              example: missing_token
            message:
              type: string
              description: Human-readable message. Subject to wording changes — do not pattern-match.
              example: "Missing tracer token (use `tracer_token` header or `Authorization: Bearer tk_...`)"
            request_id:
              type: string
              description: |
                Mirror of the `X-Request-ID` response header. Quote this
                value in support tickets so we can pinpoint the exact
                server log entry.
              example: "8c5d96f1-9e47-4c01-bb1e-8b5a7a2a1234"
      additionalProperties: true
      examples:
        - error:
            code: missing_token
            message: "Missing tracer token (use `tracer_token` header or `Authorization: Bearer tk_...`)"
            request_id: "8c5d96f1-9e47-4c01-bb1e-8b5a7a2a1234"


    # ---------- Sessions ----------
    SessionCreateRequest:
      type: object
      required: [namespace]
      properties:
        namespace:
          type: string
          description: Logical namespace the session belongs to (e.g. agent name).
          example: my-agent
        user_id:
          type: string
          description: External user identifier (used in `session_id` generation).
          example: "5521999998888"
        inactivity_timeout:
          type: integer
          minimum: 1
          default: 120
          description: Seconds of inactivity before the session expires.
        metadata:
          type: object
          additionalProperties: true
          description: Free-form metadata stored with the session.

    Session:
      type: object
      required: [session_id, namespace, created_at]
      properties:
        session_id:
          type: string
          example: my-namespace-user123-20251210123456
        namespace:
          type: string
        user_id:
          type: string
        inactivity_timeout:
          type: integer
        created_at:
          type: string
          format: date-time
        metadata:
          type: object
          additionalProperties: true

    SessionGetOrCreateRequest:
      type: object
      required: [namespace, user_id]
      properties:
        namespace:
          type: string
        user_id:
          type: string
        inactivity_timeout:
          type: integer
          minimum: 1
          default: 120
        force_new:
          type: boolean
          default: false
          description: Force a new session even if a recent active session exists.

    SessionGetOrCreateResponse:
      allOf:
        - $ref: "#/components/schemas/Session"
        - type: object
          properties:
            reused:
              type: boolean
              description: True if an existing session was returned, false if a new one was created.

    # ---------- Traces ----------
    LlmTraceRequest:
      type: object
      required: [session_id]
      properties:
        session_id:
          type: string
        model:
          type: string
          example: gpt-4
        provider:
          type: string
          example: openai
        input:
          type: object
          description: |
            Input payload. Common shape: `{ "messages": [...] }`.
          additionalProperties: true
        output:
          type: object
          description: |
            Output payload. Common shape: `{ "content": "...", "usage": {...} }`.
          additionalProperties: true
        latency_ms:
          type: integer
          minimum: 0
        metadata:
          type: object
          additionalProperties: true
        timestamp:
          type: string
          format: date-time
        external_user_id:
          type: string
          description: Optional external user identifier (phone, email, customer ID).
          example: "5521999998888"
        external_user_name:
          type: string
          description: Optional human-readable user display name.
          example: "João Silva"
        external_user_channel:
          type: string
          description: Origin channel for the user.
          enum: [whatsapp, web, telegram, slack, email, teams, sms, voice, other]
          example: whatsapp

    LlmTraceResponse:
      type: object
      properties:
        success:
          type: boolean
        trace_type:
          type: string
          const: llm_call
        tokens:
          type: object
          properties:
            input:
              type: integer
            output:
              type: integer

    ToolTraceRequest:
      type: object
      required: [session_id, tool_name]
      properties:
        session_id:
          type: string
        tool_name:
          type: string
        arguments:
          type: object
          additionalProperties: true
        result: {}
        latency_ms:
          type: integer
          minimum: 0
        agent:
          type: string
          description: Agent that called the tool.
        metadata:
          type: object
          additionalProperties: true
        timestamp:
          type: string
          format: date-time
        external_user_id:
          type: string
          description: Optional external user identifier (phone, email, customer ID).
          example: "5521999998888"
        external_user_name:
          type: string
          description: Optional human-readable user display name.
          example: "João Silva"
        external_user_channel:
          type: string
          description: Origin channel for the user.
          enum: [whatsapp, web, telegram, slack, email, teams, sms, voice, other]
          example: whatsapp

    ToolTraceResponse:
      type: object
      properties:
        success:
          type: boolean
        trace_type:
          type: string
          const: tool_call
        tool_name:
          type: string

    HandoffTraceRequest:
      type: object
      required: [session_id, from_agent, to_agent]
      properties:
        session_id:
          type: string
        from_agent:
          type: string
        to_agent:
          type: string
        reason:
          type: string
        metadata:
          type: object
          additionalProperties: true
        timestamp:
          type: string
          format: date-time
        external_user_id:
          type: string
          description: Optional external user identifier (phone, email, customer ID).
          example: "5521999998888"
        external_user_name:
          type: string
          description: Optional human-readable user display name.
          example: "João Silva"
        external_user_channel:
          type: string
          description: Origin channel for the user.
          enum: [whatsapp, web, telegram, slack, email, teams, sms, voice, other]
          example: whatsapp

    HandoffTraceResponse:
      type: object
      properties:
        success:
          type: boolean
        trace_type:
          type: string
          const: handoff
        from:
          type: string
        to:
          type: string

    LogTraceRequest:
      type: object
      required: [message]
      properties:
        session_id:
          type: string
          description: Provide either `session_id` or `namespace`.
        namespace:
          type: string
        level:
          type: string
          enum: [debug, info, warn, error]
          default: info
        message:
          type: string
        resource_id:
          type: string
        metadata:
          type: object
          additionalProperties: true
        timestamp:
          type: string
          format: date-time

    LogTraceResponse:
      type: object
      properties:
        success:
          type: boolean
        trace_type:
          type: string
          const: log

    # ---------- Records ----------
    Message:
      type: object
      required: [role]
      properties:
        role:
          type: string
          enum: [user, assistant, system, tool]
        content:
          oneOf:
            - type: string
            - type: array
              items:
                type: object
                additionalProperties: true
            - type: object
              additionalProperties: true
            - type: "null"
        sender:
          type: string
        tool_calls:
          type: array
          items:
            type: object
            additionalProperties: true
        tool_call_id:
          type: string
        tool_name:
          type: string
        is_internal_tool:
          type: boolean
        internal_tool_type:
          type: string
          enum: [web_search_call, file_search_call, code_interpreter_call]

    MessageList:
      type: array
      description: Ordered list of messages (turn or session segment).
      items:
        $ref: "#/components/schemas/Message"

    MessageDict:
      type: object
      description: Free-form message-shaped payload (escape hatch for legacy clients).
      additionalProperties: true

    Transfer:
      type: object
      required: [from, to]
      properties:
        from:
          type: string
          description: Source agent.
        to:
          type: string
          description: Target agent.
        reason:
          type: string
        timestamp:
          type: string

    ConversationRecord:
      type: object
      required: [namespace, agent, msg]
      properties:
        namespace:
          type: string
        agent:
          type: string
        session_id:
          type: string
        msg:
          oneOf:
            - $ref: "#/components/schemas/Message"
            - $ref: "#/components/schemas/MessageList"
            - $ref: "#/components/schemas/MessageDict"
        input_tokens:
          type: integer
          minimum: 0
        output_tokens:
          type: integer
          minimum: 0
        process_tokens:
          type: integer
          minimum: 0
        memory_tokens:
          type: integer
          minimum: 0
        total_tokens:
          type: integer
          minimum: 0
        transfers:
          type: array
          items:
            $ref: "#/components/schemas/Transfer"
        inserted_at:
          type: string
          format: date-time
        user_id:
          type: string
        user_whatsapp:
          type: string
        wam_id:
          type: string
        replied_whatsapp:
          type: string
        attachments:
          type: array
          items: {}
        source:
          type: string
          description: Source tool (claude-code, cline, copilot, openai-agents, langchain, ...).
        model:
          type: string
        external_user_id:
          type: string
          description: Optional external user identifier (phone, email, customer ID).
          example: "5521999998888"
        external_user_name:
          type: string
          description: Optional human-readable user display name.
          example: "João Silva"
        external_user_channel:
          type: string
          description: Origin channel for the user.
          enum: [whatsapp, web, telegram, slack, email, teams, sms, voice, other]
          example: whatsapp

    RecordsUploadRequest:
      type: object
      required: [records]
      properties:
        records:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/ConversationRecord"

    RecordsUploadResponse:
      type: object
      properties:
        inserted_count:
          type: integer
          minimum: 0
        dropped_count:
          type: integer
          minimum: 0
          description: Records dropped server-side (e.g., duplicates).
        details:
          type: object
          additionalProperties: true

    # ---------- Logs ----------
    LogEntry:
      type: object
      required: [namespace, level, message]
      properties:
        namespace:
          type: string
        level:
          type: string
          enum: [debug, info, warn, error]
        message:
          type: string
        timestamp:
          type: string
          format: date-time
        resource_id:
          type: string
        custom_object:
          type: object
          additionalProperties: true

    LogsUploadRequest:
      type: object
      required: [logs]
      properties:
        logs:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/LogEntry"

    LogsUploadResponse:
      type: object
      additionalProperties: true
      properties:
        inserted_count:
          type: integer
          minimum: 0

    # ---------- Query ----------
    RecordQueryFilters:
      type: object
      properties:
        agent:
          type: string
        session_id:
          type: string
        start_date:
          type: string
          format: date-time
        end_date:
          type: string
          format: date-time
        limit:
          type: integer
          minimum: 1
          maximum: 1000
          default: 100
        offset:
          type: integer
          minimum: 0
          default: 0

    RecordQueryRequest:
      type: object
      required: [namespace, query]
      properties:
        namespace:
          type: string
        query:
          $ref: "#/components/schemas/RecordQueryFilters"

    RecordQueryResponse:
      type: object
      properties:
        records:
          type: array
          items:
            $ref: "#/components/schemas/ConversationRecord"
        count:
          type: integer
          minimum: 0

    LogQueryRequest:
      type: object
      required: [namespace]
      properties:
        namespace:
          type: string
        level:
          type: string
          enum: [debug, info, warn, error]
        resource_id:
          type: string
        start_date:
          type: string
          format: date-time
        end_date:
          type: string
          format: date-time
        limit:
          type: integer
          minimum: 1
          maximum: 1000
          default: 100
        offset:
          type: integer
          minimum: 0
          default: 0

    LogQueryResponse:
      type: object
      properties:
        logs:
          type: array
          items:
            $ref: "#/components/schemas/LogEntry"
        count:
          type: integer
          minimum: 0

    # ---------- Export ----------
    RecordExportRequest:
      type: object
      required: [namespace]
      properties:
        namespace:
          type: string
        agent:
          type: string
        session_id:
          type: string
        start_date:
          type: string
          format: date-time
        end_date:
          type: string
          format: date-time
        format:
          type: string
          enum: [json, csv]
          default: json

    RecordExportResponse:
      type: object
      properties:
        records:
          type: array
          items:
            $ref: "#/components/schemas/ConversationRecord"

    LogExportRequest:
      type: object
      required: [namespace]
      properties:
        namespace:
          type: string
        level:
          type: string
          enum: [debug, info, warn, error]
        resource_id:
          type: string
        start_date:
          type: string
          format: date-time
        end_date:
          type: string
          format: date-time
        format:
          type: string
          enum: [json, csv]
          default: json

    LogExportResponse:
      type: object
      properties:
        logs:
          type: array
          items:
            $ref: "#/components/schemas/LogEntry"
