openapi: 3.1.0
info:
  title: agentic-works API (Combined)
  version: 0.5.0
servers:
  - url: http://localhost:8000
paths:
  /health:
    get:
      summary: Health check
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string }
  /version:
    get:
      summary: API version
      responses:
        '200':
          description: version
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { type: string }
  /threads/{thread_id}/stream:
    get:
      summary: Heartbeat SSE stream
      parameters:
        - in: path
          name: thread_id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: SSE stream of heartbeat events
          content:
            text/event-stream:
              schema:
                $ref: '#/components/schemas/SseEvent'
              examples:
                heartbeat:
                  value: { event: heartbeat, data: { ts: '2025-01-01T00:00:00Z' } }
  /ai/chat/stream:
    post:
      summary: Chat SSE stream (alias)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                message: { type: string }
                profile_id: { type: string, nullable: true }
              required: [message]
      responses:
        '200':
          description: SSE stream of chat events
          content:
            text/event-stream:
              schema:
                $ref: '#/components/schemas/SseEvent'
              examples:
                started:
                  value: { event: state, data: { status: started } }
                chunk:
                  value: { event: chunk, data: { text: "Hello" } }
                citations:
                  value: { event: citations, data: { items: [] } }
                usage:
                  value: { event: usage, data: { input_tokens: 3, output_tokens: 2, cost_usd: 0.0 } }
  /chat/stream:
    post:
      summary: Chat SSE stream
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                message: { type: string }
                profile_id: { type: string, nullable: true }
              required: [message]
      responses:
        '200':
          description: SSE stream of chat events
          content:
            text/event-stream:
              schema:
                $ref: '#/components/schemas/SseEvent'
              examples:
                started:
                  value: { event: state, data: { status: started } }
                chunk:
                  value: { event: chunk, data: { text: "Echo: hi" } }
                citations:
                  value: { event: citations, data: { items: [] } }
                usage:
                  value: { event: usage, data: { input_tokens: 3, output_tokens: 3, cost_usd: 0.0 } }
  /chat/cancel:
    post:
      summary: Cancel chat (stub)
      requestBody:
        required: false
      responses:
        '202':
          description: Accepted
  /ai/models:
    get:
      summary: List available models
      parameters:
        - in: query
          name: mode
          required: false
          schema: { type: string, enum: [chat, embedding] }
        - in: query
          name: provider
          required: false
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/ModelInfo'
                required: [items]
  /ai/profiles:
    get:
      summary: List AI profiles (alias)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ProfileOut'
  /ai/profiles/{id}:
    get:
      summary: Get AI profile by id (alias)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileOut'
        '404': { description: Not Found }
  /ai/embeddings:
    post:
      summary: Create embeddings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmbeddingRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmbeddingResponse'
  /ai/profiles/{id}/validate:
    post:
      summary: Validate AI profile configuration
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateProfileResponse'
        '404': { description: Not Found }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /jobs/{job_id}/stream:
    get:
      summary: Job progress stream (SSE)
      parameters:
        - in: path
          name: job_id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: SSE stream of job progress events
          content:
            text/event-stream:
              schema:
                type: string
              examples:
                job:
                  value: { event: job, data: { id: "job-1", status: running, progress: 50 } }
                state:
                  value: { event: state, data: { status: completed } }
  /audit/events:
    get:
      summary: List recent audit events (read-only)
      parameters:
        - in: query
          name: limit
          schema: { type: integer, default: 100, minimum: 1, maximum: 1000 }
        - in: query
          name: actor_id
          schema: { type: string }
        - in: query
          name: org_id
          schema: { type: string }
        - in: query
          name: project_id
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AuditEvent'
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /admin/instance:
    get:
      summary: Get instance configuration (read-only)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InstanceConfigOut'
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
    patch:
      summary: Update instance configuration (dev only)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                required: [ok]
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
        '409': { description: Not Configured }
  /tools:
    get:
      summary: List dev tools
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ToolOut'
    post:
      summary: Register dev tool (echo)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ToolSpec'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolOut'
  /functions:
    get:
      summary: List registered functions
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/FunctionOut'
    post:
      summary: Register a function (in-memory)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FunctionSpec'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties: { id: { type: string } }
                required: [id]
  /functions/{id}:
    get:
      summary: Get function by id
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FunctionOut'
        '404': { description: Not Found }
  /functions/{id}/invoke:
    post:
      summary: Invoke a function
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvokeRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvokeResponse'
            text/event-stream:
              schema:
                $ref: '#/components/schemas/SseEvent'
              examples:
                heartbeat:
                  value: { event: heartbeat, data: { ts: '2025-01-01T00:00:00Z' } }
                result:
                  value: { event: result, data: { any: 'payload' } }
  /functions/{id}/test:
    post:
      summary: Test a function (dev)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                args: { type: object }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvokeResponse'
  /connectors:
    get:
      summary: List supported connector types
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConnectorInfo'
                required: [items]
  /connectors/{type}/test:
    post:
      summary: Test a connector configuration
      parameters:
        - in: path
          name: type
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConnectorTestRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectorTestResponse'
  /connectors/health:
    get:
      summary: Connectors health (dev)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectorHealthResponse'
  /authz/whoami:
    get:
      summary: Show principal context (dev)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WhoAmI'
  /authz/roles:
    get:
      summary: List supported roles (dev)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RolesList'
  /auth/login:
    post:
      summary: Dev login (returns dev bearer token)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email: { type: string, format: email }
                org_id: { type: string, nullable: true }
                role: { type: string, enum: [tm, td, ta, sa, org_admin, project_owner, editor, member, viewer], default: tm }
                project_id: { type: string, nullable: true }
              required: [email]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
                  note: { type: string, nullable: true }
                required: [token]
  /auth/magic/send:
    post:
      summary: Send magic link to email (dev SMTP)
      parameters:
        - in: query
          name: email
          required: true
          schema: { type: string, format: email }
      responses:
        '200': { description: OK }
        '400': { description: Bad Request }
        '409': { description: Disabled }
        '502': { description: SMTP Error }
  /auth/magic/callback:
    get:
      summary: Magic link callback (issues dev bearer token)
      parameters:
        - in: query
          name: token
          required: true
          schema: { type: string }
      responses:
        '200': { description: OK }
        '400': { description: Invalid Token }
  /auth/oauth/google/start:
    get:
      summary: Start Google OIDC flow (returns authorization URL)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  url: { type: string }
                required: [url]
        '409': { description: Not Configured }
  /auth/oauth/google/callback:
    get:
      summary: Google OIDC callback (issues dev bearer token)
      parameters:
        - in: query
          name: code
          required: true
          schema: { type: string }
        - in: query
          name: state
          required: true
          schema: { type: string }
      responses:
        '200': { description: OK }
        '400': { description: Invalid State/Token }
        '409': { description: Not Configured }
        '502': { description: Upstream Error }
  /usage/events:
    get:
      summary: List recent usage events
      parameters:
        - in: query
          name: limit
          schema: { type: integer, default: 50 }
        - in: query
          name: kind
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/UsageEventOut'
  /usage/summary:
    get:
      summary: Usage summary
      parameters:
        - in: query
          name: scope
          required: true
          schema: { type: string, enum: [org, project, profile] }
        - in: query
          name: id
          required: true
          schema: { type: string }
        - in: query
          name: period
          required: false
          schema: { type: string, enum: [daily, weekly, monthly, rolling30], default: rolling30 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  total_tokens:
                    type: object
                    properties:
                      input: { type: integer }
                      output: { type: integer }
                  cost_usd: { type: number }
                  events: { type: integer }
                required: [total_tokens, cost_usd, events]
  /usage/budgets:
    get:
      summary: List budgets
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Budget'
    post:
      summary: Create budget
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Budget'
      responses:
        '201': { description: Created }
  /usage/budgets/{id}:
    get:
      summary: Get budget
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Budget'
        '404': { description: Not Found }
    patch:
      summary: Update budget
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Budget'
      responses:
        '200': { description: OK }
    delete:
      summary: Delete budget
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '204': { description: No Content }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    oauth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://accounts.google.com/o/oauth2/v2/auth
          tokenUrl: https://oauth2.googleapis.com/token
          scopes:
            openid: OpenID Connect scope
            email: Access email address
            profile: Access basic profile
  schemas:
    ModelInfo:
      type: object
      properties:
        id: { type: string }
        provider: { type: string }
        family: { type: string }
        context_window: { type: integer, nullable: true }
        supports_tools: { type: boolean, nullable: true }
        price_input: { type: number, nullable: true }
        price_output: { type: number, nullable: true }
        price_embedding: { type: number, nullable: true }
        rate_limits:
          type: object
          properties:
            tpm: { type: integer }
            rpm: { type: integer }
        tags:
          type: array
          items: { type: string }
      required: [id, provider]
    ProfileOut:
      type: object
      properties:
        id: { type: string }
        kind: { type: string, enum: [chat, embed] }
        provider: { type: string }
        model: { type: string }
        base_url: { type: string, nullable: true }
        api_key_cred_id: { type: string, nullable: true }
        pricing: { type: object, nullable: true }
        limits: { type: object, nullable: true }
        is_default: { type: boolean }
        created_at: { type: string, format: date-time }
      required: [id, kind, provider, model, is_default, created_at]
    EmbeddingRequest:
      type: object
      properties:
        input:
          type: array
          items: { type: string }
        profile_id: { type: string, nullable: true }
      required: [input]
    EmbeddingResponse:
      type: object
      properties:
        data:
          type: array
          items:
            type: object
            properties:
              index: { type: integer }
              embedding:
                type: array
                items: { type: number }
        model: { type: string }
      required: [data, model]
    ValidateProfileResponse:
      type: object
      properties:
        ok: { type: boolean }
        checks:
          type: object
          properties:
            exists: { type: boolean }
            provider: { type: boolean }
            model: { type: boolean }
            api_key: { type: boolean }
            base_url: { type: boolean }
          required: [exists, provider, model]
      required: [ok, checks]
    AuditEvent:
      type: object
      additionalProperties: true
    InstanceConfigOut:
      type: object
      properties:
        path: { type: string, nullable: true }
        writable: { type: boolean }
        config: { type: object }
      required: [writable, config]
    ToolSpec:
      type: object
      properties:
        id: { type: string }
        args_schema: { type: object }
        mode: { type: string, enum: [echo] }
      required: [id, args_schema]
    ToolOut:
      type: object
      properties:
        id: { type: string }
        args_schema: { type: object }
      required: [id, args_schema]
    FunctionSpec:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        description: { type: string }
        args_schema: { type: object }
        connector:
          type: object
          properties:
            type: { type: string, enum: [http, slack, email] }
            config: { type: object }
          required: [type]
        policy:
          type: object
          properties:
            timeout_ms: { type: integer }
            retry:
              type: object
              properties:
                max_retries: { type: integer }
                backoff_ms: { type: integer }
      required: [id, name, args_schema, connector]
    FunctionOut:
      allOf:
        - $ref: '#/components/schemas/FunctionSpec'
        - type: object
          properties:
            tags:
              type: array
              items: { type: string }
    InvokeRequest:
      type: object
      properties:
        args: { type: object }
        stream: { type: boolean, default: false }
        profile_id: { type: string, nullable: true }
      required: [args]
    InvokeResponse:
      type: object
      properties:
        ok: { type: boolean }
        data: { nullable: true }
        error:
          type: object
          nullable: true
          properties:
            code: { type: string }
            message: { type: string }
        meta:
          type: object
          properties:
            latency_ms: { type: integer }
      required: [ok]
    ConnectorInfo:
      type: object
      properties:
        type: { type: string }
        capabilities:
          type: array
          items: { type: string }
      required: [type]
    ConnectorTestRequest:
      type: object
      properties:
        config: { type: object }
        credentials_ref: { type: string, nullable: true }
      required: [config]
    ConnectorTestResponse:
      type: object
      properties:
        ok: { type: boolean }
        error: { type: string, nullable: true }
      required: [ok]
    ConnectorHealthResponse:
      type: object
      properties:
        ok: { type: boolean }
        connectors:
          type: array
          items:
            type: object
            properties:
              type: { type: string }
              status: { type: string }
            required: [type, status]
      required: [ok, connectors]
    UsageEventOut:
      type: object
      properties:
        id: { type: integer }
        kind: { type: string }
        profile_id: { type: string, nullable: true }
        org_id: { type: string, nullable: true }
        project_id: { type: string, nullable: true }
        input_tokens: { type: integer }
        output_tokens: { type: integer }
        cost_usd: { type: number }
        trace_id: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
      required: [id, kind, input_tokens, output_tokens, cost_usd, created_at]
    Budget:
      type: object
      properties:
        id: { type: string }
        scope: { type: string, enum: [org, project, profile] }
        ref_id: { type: string }
        period: { type: string, enum: [daily, weekly, monthly, rolling30] }
        limit_usd: { type: number }
        warn_pct: { type: number, default: 0.9 }
        block_on_exceed: { type: boolean, default: true }
      required: [id, scope, ref_id, period, limit_usd]
    SseEvent:
      type: object
      properties:
        event: { type: string }
        data: { type: object }
      required: [event, data]
      description: Single SSE event envelope; emitted repeatedly on a streaming response.
    WhoAmI:
      type: object
      properties:
        role: { type: string, nullable: true }
        org_id: { type: string, nullable: true }
        project_id: { type: string, nullable: true }
    RolesList:
      type: object
      properties:
        roles:
          type: array
          items: { type: string }
      required: [roles]
  /experts:
    get:
      summary: List experts
      parameters:
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Expert' }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
    post:
      summary: Create expert
      parameters:
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin] }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Expert' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Expert' }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /experts/{id}:
    get:
      summary: Get expert
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Expert' }
        '404': { description: Not Found }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
    patch:
      summary: Update expert
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin] }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Expert' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Expert' }
        '404': { description: Not Found }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
    delete:
      summary: Delete expert
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin] }
      responses:
        '204': { description: No Content }
        '404': { description: Not Found }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /projects:
    get:
      summary: List projects
      parameters:
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Project' }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
    post:
      summary: Create project
      parameters:
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin] }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Project' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /projects/{id}:
    get:
      summary: Get project
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }
        '404': { description: Not Found }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
    patch:
      summary: Update project
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin] }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Project' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }
        '404': { description: Not Found }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
    delete:
      summary: Delete project
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin] }
      responses:
        '204': { description: No Content }
        '404': { description: Not Found }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /threads:
    get:
      summary: List threads
      parameters:
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ThreadOut' }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
    post:
      summary: Create thread
      parameters:
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ThreadOut' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ThreadOut' }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /threads/{id}:
    get:
      summary: Get thread
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ThreadOut' }
        '404': { description: Not Found }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /threads/{id}/messages:
    post:
      summary: Post message to thread (SSE)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content: { type: string }
                speaker: { type: string, enum: [user, assistant, tool] }
                expert_id: { type: string, nullable: true }
              required: [content]
      responses:
        '200':
          description: SSE stream of assistant reply
          content:
            text/event-stream:
              schema:
                $ref: '#/components/schemas/SseEvent'
              examples:
                started:
                  value: { event: state, data: { status: started } }
                chunk:
                  value: { event: chunk, data: { text: 'hello' } }
                citations:
                  value: { event: citations, data: { items: [] } }
                usage:
                  value: { event: usage, data: { input_tokens: 3, output_tokens: 3, cost_usd: 0.0 } }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /threads/{id}/wrapup:
    get:
      summary: Get thread wrap-up (markdown)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  markdown: { type: string }
                  download_url: { type: string }
                required: [markdown]
        '404': { description: Not Found }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /threads/{id}/export:
    get:
      summary: Export thread (Markdown)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      responses:
        '200':
          description: OK
          content:
            text/markdown:
              schema:
                type: string
        '404': { description: Not Found }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
        '404': { description: Not Found }
    get:
      summary: List thread messages
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/MessageOut' }
        '404': { description: Not Found }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /tasks:
    get:
      summary: List tasks
      parameters:
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/TaskOut' }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
    post:
      summary: Create task
      parameters:
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TaskOut' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TaskOut' }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /tasks/{id}:
    get:
      summary: Get task
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TaskOut' }
        '404': { description: Not Found }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
  /tasks/{id}/runs:
    post:
      summary: Create run
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                id: { type: string }
                input: { type: object }
              required: [id]
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RunOut' }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
        '404': { description: Not Found }
  /tasks/{id}/runs/{run_id}:
    get:
      summary: Get run
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: path
          name: run_id
          required: true
          schema: { type: string }
        - in: header
          name: X-Role
          required: true
          schema: { type: string, enum: [org_admin, member] }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RunOut' }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
        '404': { description: Not Found }
  Expert:
    type: object
    properties:
      id: { type: string }
      name: { type: string }
      profile_id: { type: string, nullable: true }
      dataset_ids: { type: array, items: { type: string }, nullable: true }
      function_ids: { type: array, items: { type: string }, nullable: true }
      policies: { type: object, nullable: true }
      created_at: { type: string, format: date-time }
    required: [id, name, created_at]
  Project:
    type: object
    properties:
      id: { type: string }
      name: { type: string }
      status: { type: string }
      policies: { type: object, nullable: true }
      tags: { type: array, items: { type: string }, nullable: true }
      created_at: { type: string, format: date-time }
    required: [id, name, status, created_at]
  ThreadOut:
    type: object
    properties:
      id: { type: string }
      project_id: { type: string, nullable: true }
      routing_policy: { type: object, nullable: true }
      status: { type: string }
      context_version: { type: integer }
      created_at: { type: string, format: date-time }
    required: [id, status, context_version, created_at]
  TaskOut:
    type: object
    properties:
      id: { type: string }
      project_id: { type: string, nullable: true }
      thread_id: { type: string, nullable: true }
      status: { type: string }
      created_at: { type: string, format: date-time }
    required: [id, status, created_at]
  RunOut:
    type: object
    properties:
      id: { type: string }
      task_id: { type: string }
      status: { type: string }
      started_at: { type: string, format: date-time }
      finished_at: { type: string, format: date-time, nullable: true }
      snapshot_start: { type: object, nullable: true }
      snapshot_end: { type: object, nullable: true }
      outputs_md: { type: string, nullable: true }
      outputs_json: { type: object, nullable: true }
    required: [id, task_id, status, started_at]
