openapi: 3.1.0
info:
  title: Delvry Protocol
  version: 0.1.0
  description: |
    The Delvry Protocol: a JSON-over-HTTP standard for agent ↔ human context.

    Agents query a workspace's context graph for what humans in an organization
    know, write claims when they learn things, and request fresh harvests when
    the graph has gaps. Delvry's hosted implementation runs at delvry.ai;
    third-party implementations are encouraged.

    All endpoints are scoped to a single workspace identified by the API key.

    This spec is **draft v0.1.0**. RFC-style feedback welcome.
  license:
    name: MIT
servers:
  - url: https://delvry.ai/v1
    description: Reference (hosted) implementation
security:
  - apiKey: []
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: opaque
  schemas:
    EntityType:
      type: string
      enum:
        - person
        - role
        - team
        - process
        - system
        - policy
        - product
        - customer_segment
        - decision
        - principle
        - other
    DecayClass:
      type: string
      enum: [principle, policy, process, tactical, transient]
      description: |
        Half-lives: principle 18mo, policy 6mo, process 3mo, tactical 1mo,
        transient 2wk. Determines when a claim is eligible for nightly
        re-verification.
    ClaimStatus:
      type: string
      enum: [active, contested, canonical, deprecated]
    SourceMethod:
      type: string
      enum: [survey, inferred, imported, human_edited, agent_written]
    EntityRef:
      type: object
      required: [type, canonical_name]
      properties:
        type: { $ref: "#/components/schemas/EntityType" }
        canonical_name:
          type: string
          description: snake_case workspace-unique identifier within type
        aliases:
          type: array
          items: { type: string }
        description: { type: string }
    Entity:
      allOf:
        - $ref: "#/components/schemas/EntityRef"
        - type: object
          required: [id]
          properties:
            id: { type: string }
    Source:
      type: object
      required: [method, quote, asserted_at]
      properties:
        id: { type: string }
        method: { $ref: "#/components/schemas/SourceMethod" }
        recipient_id: { type: string, nullable: true }
        respondent_entity_id: { type: string, nullable: true }
        thread_id: { type: string, nullable: true }
        inquiry_id: { type: string, nullable: true }
        external_source_type: { type: string, nullable: true }
        external_source_ref: { type: string, nullable: true }
        quote: { type: string }
        asserted_at:
          type: integer
          format: int64
          description: unix-ms timestamp the source asserted this claim
        supports:
          type: integer
          enum: [0, 1]
          description: 1 if this source supports the claim, 0 if it contradicts
    Conflict:
      type: object
      required: [id, status, detected_at, claim_ids]
      properties:
        id: { type: string }
        status:
          type: string
          enum:
            [
              open,
              resolved_by_source,
              resolved_by_canon,
              resolved_by_followup,
              unresolvable,
            ]
        axis: { type: string, nullable: true }
        detected_at: { type: integer, format: int64 }
        resolved_at: { type: integer, format: int64, nullable: true }
        claim_ids:
          type: array
          items: { type: string }
    Claim:
      type: object
      required:
        - id
        - subject_entity
        - predicate
        - object
        - object_text
        - confidence
        - status
        - decay_class
        - created_at
        - verified_at
        - sources
        - conflicts
      properties:
        id: { type: string }
        subject_entity: { $ref: "#/components/schemas/Entity" }
        predicate:
          type: string
          description: snake_case property identifier (e.g. refund_window_days)
        object:
          description: structured value (number, string, object, array)
        object_text:
          type: string
          description: human-readable rendering — also what gets embedded
        confidence:
          type: number
          minimum: 0
          maximum: 1
        status: { $ref: "#/components/schemas/ClaimStatus" }
        decay_class: { $ref: "#/components/schemas/DecayClass" }
        related_entity_ids:
          type: array
          items: { type: string }
        created_at: { type: integer, format: int64 }
        verified_at: { type: integer, format: int64 }
        similarity:
          type: number
          minimum: 0
          maximum: 1
          description: present on semantic-search results
        sources:
          type: array
          items: { $ref: "#/components/schemas/Source" }
        conflicts:
          type: array
          items: { $ref: "#/components/schemas/Conflict" }
    QueryResult:
      type: object
      required: [claims, graph_gap, used_mode]
      properties:
        claims:
          type: array
          items: { $ref: "#/components/schemas/Claim" }
        graph_gap:
          type: boolean
          description: |
            True when no result meets the requested min_confidence (or no
            result at all). Signals the agent should harvest the gap from
            humans. There is no `/graph/ask_humans` REST endpoint — that name
            refers to the MCP-only combo tool `delvry_ask_humans`, which
            internally re-runs `/graph/query` and, if the gap is real, creates
            a thread + inquiry in a single call. REST clients reproduce it by
            calling `POST /graph/query`, then on `graph_gap: true` calling
            `POST /v1/threads` followed by `POST /v1/threads/{id}/inquiries`
            with the gap framed as a single question item.
        used_mode:
          type: string
          enum: [semantic, structured, entity_direct]
paths:
  /graph/query:
    post:
      summary: Query the context graph
      description: |
        Semantic + structured search. Pass `question` for natural-language
        retrieval (uses vector similarity), or `filters` to narrow by
        subject_entity_id / predicate / decay_class / min_confidence.
        Combine both to tighten results.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                question: { type: string, maxLength: 2000 }
                filters:
                  type: object
                  properties:
                    subject_entity_id: { type: string }
                    predicate: { type: string }
                    decay_class:
                      type: array
                      items: { $ref: "#/components/schemas/DecayClass" }
                    status:
                      type: array
                      items: { $ref: "#/components/schemas/ClaimStatus" }
                    min_confidence:
                      type: number
                      minimum: 0
                      maximum: 1
                    include_deprecated: { type: boolean }
                limit:
                  type: integer
                  minimum: 1
                  maximum: 50
                  default: 10
      responses:
        "200":
          description: matching claims
          content:
            application/json:
              schema: { $ref: "#/components/schemas/QueryResult" }

  /graph/claims:
    post:
      summary: Write an agent-derived claim into the graph
      description: |
        Contribute a claim the agent learned from a non-human source (code,
        logs, file analysis). Confidence is hard-capped at 0.7; agent-only
        claims cannot reach canonical without subsequent human confirmation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - subject_entity
                - predicate
                - object
                - object_text
                - decay_class
                - confidence
                - quote
              properties:
                subject_entity: { $ref: "#/components/schemas/EntityRef" }
                related_entities:
                  type: array
                  items: { $ref: "#/components/schemas/EntityRef" }
                respondent_entity: { $ref: "#/components/schemas/EntityRef" }
                predicate: { type: string }
                object: {}
                object_text: { type: string }
                decay_class: { $ref: "#/components/schemas/DecayClass" }
                confidence: { type: number, minimum: 0, maximum: 1 }
                quote: { type: string }
                source_ref: { type: string }
      responses:
        "200":
          description: claim applied
          content:
            application/json:
              schema:
                type: object
                properties:
                  result:
                    type: string
                    enum: [written, confirmed, conflict, skipped]

  /graph/verify:
    post:
      summary: Force re-verification of a specific claim
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [claim_id]
              properties:
                claim_id: { type: string }
                audience_hint: { type: string }
      responses:
        "200":
          description: re-verification queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  queued: { type: boolean }
                  thread_id: { type: string }
                  inquiry_id: { type: string }
        "404":
          description: no live human source available

  /graph/ingest:
    post:
      summary: Ingest external text (meeting transcript, slack, doc)
      description: |
        Chunks the text and runs each chunk through claim extraction with
        method='imported' and confidence capped at 0.6. Returns counts so
        the caller can decide whether to verify imported claims via humans.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [source_type, source_ref, text]
              properties:
                source_type:
                  type: string
                  enum: [meeting_transcript, slack, notion, doc, other]
                source_ref: { type: string }
                text: { type: string, maxLength: 200000 }
                speakers:
                  type: array
                  items: { type: string }
                recorded_at: { type: string, format: date-time }
      responses:
        "200":
          description: ingest complete
          content:
            application/json:
              schema:
                type: object
                properties:
                  chunks_processed: { type: integer }
                  claims_written: { type: integer }
                  claims_confirmed: { type: integer }
                  conflicts_detected: { type: integer }
                  errors: { type: integer }

  /graph/subscriptions:
    post:
      summary: Subscribe to graph events
      description: |
        Register a webhook for claim/conflict events on an entity or
        predicate. Delvry POSTs to `callback_url` with payloads signed by
        `callback_secret` (HMAC-SHA256, via Delvry-Signature header).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [target_kind, target_value, callback_url]
              properties:
                target_kind:
                  type: string
                  enum: [entity, predicate]
                target_value: { type: string }
                callback_url: { type: string, format: uri }
                callback_secret: { type: string, minLength: 16 }
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - claim.created
                      - claim.confidence_changed
                      - conflict.detected
                      - conflict.resolved
      responses:
        "201":
          description: subscription created
    get:
      summary: List subscriptions
      responses:
        "200":
          description: subscriptions
  /graph/subscriptions/{id}:
    delete:
      summary: Revoke a subscription
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: revoked
