{
  "openapi": "3.1.0",
  "info": {
    "title": "abagraph API",
    "version": "0.1.0",
    "description": "abagraph is the memory layer (AIhouse) for AI agents: a managed, bitemporal, source-backed memory database. Every claim is a subject-predicate-object fact carrying valid-time plus record-time and calibrated confidence with provenance; contradictions close the prior fact instead of overwriting it, so the history IS the data. Agents read, write, reason over, and time-travel through this store over a REST API and MCP, and load task memory in one call with build_context (POST /api/context), which returns a compacted, confidence-ranked, token-budgeted packet instead of stuffing the prompt. Early access; proprietary, managed engine.",
    "contact": {
      "email": "hello@abagraph.com",
      "url": "https://abagraph.com/contact"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://abagraph.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://abagraph.com",
      "description": "apex"
    },
    {
      "url": "https://{tenant}.abagraph.com",
      "description": "tenant",
      "variables": {
        "tenant": {
          "default": "nas",
          "description": "Tenant subdomain. A tenant API key only authenticates on its own subdomain (e.g. nas.abagraph.com); reads are scoped to that tenant and writes are stamped with it."
        }
      }
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    { "name": "Health", "description": "Liveness and aggregate stats." },
    { "name": "Facts", "description": "Assert and query bitemporal, source-backed facts." },
    { "name": "Context", "description": "Compacted, confidence-ranked context packets for agent tasks." },
    { "name": "Recall", "description": "Hybrid recall across semantic, graph, and pattern paths." },
    { "name": "Connectors", "description": "Source connectors that digest external data into provenance-bearing facts." },
    { "name": "Monitoring", "description": "Trust alerts and scheduled reports." },
    { "name": "Ingestion", "description": "Digest content into extracted, provenance-bearing facts." },
    { "name": "Streaming", "description": "Live change feed over Server-Sent Events." },
    { "name": "Ask", "description": "Natural-language questions grounded in memory." },
    { "name": "MCP", "description": "Model Context Protocol over the Streamable HTTP transport (JSON-RPC 2.0); manifest at /.well-known/mcp.json." },
    { "name": "Auth", "description": "Cookie-session login for browser clients." }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "tags": ["Health"],
        "summary": "Liveness probe",
        "description": "Public liveness probe. Returns a tiny JSON body and never requires authentication.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service is live.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Health" },
                "example": { "ok": true }
              }
            }
          }
        }
      }
    },
    "/api/stats": {
      "get": {
        "operationId": "getStats",
        "tags": ["Health"],
        "summary": "Aggregate counts for the tenant",
        "description": "Tenant-scoped aggregate counts: facts by status, distinct entities and predicates, and the engine version. Admin tokens see whole-graph totals.",
        "responses": {
          "200": {
            "description": "Aggregate counts.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Stats" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/facts": {
      "get": {
        "operationId": "listFacts",
        "tags": ["Facts"],
        "summary": "Query facts",
        "description": "Pattern query over facts, filtered by subject, predicate, and status, and optionally time-pinned with as_of. Results are tenant-scoped.",
        "parameters": [
          {
            "name": "subject",
            "in": "query",
            "required": false,
            "description": "Exact subject (entity ref) to match, conventionally \"Type:id\" (e.g. \"Person:sarah\").",
            "schema": { "type": "string" }
          },
          {
            "name": "predicate",
            "in": "query",
            "required": false,
            "description": "Exact predicate (relation/attribute) to match (e.g. \"works_at\").",
            "schema": { "type": "string" }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Status filter. May be a comma-separated list of active, candidate, superseded, retracted. Defaults to active.",
            "schema": {
              "type": "string",
              "example": "active,candidate"
            }
          },
          {
            "name": "as_of",
            "in": "query",
            "required": false,
            "description": "Time-travel instant in unix milliseconds. Rewinds the read to the database as it stood at this time, including facts since superseded.",
            "schema": { "type": "integer", "format": "int64" }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching facts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/Fact" }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "post": {
        "operationId": "assertFact",
        "tags": ["Facts"],
        "summary": "Assert a fact",
        "description": "Assert a fact. Only subject and predicate are required; the tenant is stamped from the auth context, not the body. Facts are immutable: asserting a contradicting fact on a functional predicate closes (supersedes) the prior fact rather than overwriting it. A fact below the candidate confidence threshold (default 0.75) is stored as a candidate and supersedes nothing.",
        "parameters": [ { "$ref": "#/components/parameters/IdempotencyKey" } ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/FactInput" },
              "example": {
                "subject": "Person:sarah",
                "predicate": "works_at",
                "object": "Org:acme",
                "confidence": 0.95,
                "source": "hr-feed"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The asserted fact, stamped with id, status, and record time.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Fact" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/context": {
      "post": {
        "operationId": "buildContext",
        "tags": ["Context"],
        "summary": "Build a context packet",
        "description": "Build a compacted, confidence-ranked, source-backed context packet for an agent goal: indexed fact lookup plus a one-hop graph walk plus adjacent narrative snippets, fused and ranked under a token budget. This is the automatic context-window compaction capability and the recommended way to load memory at the start of a task; prefer one build_context call over many recall calls.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ContextRequest" },
              "example": {
                "seeds": ["Creator:maya"],
                "goal": "brief me on Maya before our sync",
                "token_budget": 80
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A context packet.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ContextPacket" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/search": {
      "get": {
        "operationId": "search",
        "tags": ["Recall"],
        "summary": "Semantic recall (query params)",
        "description": "Hybrid semantic recall: embeds the question and ranks facts by cosine similarity over fact embeddings. Returns empty unless an embedding provider is configured and facts were embedded on assert. Results are tenant-scoped.",
        "parameters": [
          {
            "name": "question",
            "in": "query",
            "required": false,
            "description": "Natural-language query to embed and match.",
            "schema": { "type": "string" }
          },
          {
            "name": "top_k",
            "in": "query",
            "required": false,
            "description": "Maximum number of facts to return, ranked by similarity.",
            "schema": { "type": "integer" }
          },
          {
            "name": "as_of",
            "in": "query",
            "required": false,
            "description": "Time-travel instant in unix milliseconds.",
            "schema": { "type": "integer", "format": "int64" }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked recall result.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SearchResult" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "post": {
        "operationId": "searchPost",
        "tags": ["Recall"],
        "summary": "Semantic recall (JSON body)",
        "description": "Same hybrid semantic recall as GET /api/search, with parameters supplied in a JSON body.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SearchRequest" },
              "example": { "question": "where does sarah work", "top_k": 8 }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ranked recall result.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SearchResult" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/graph": {
      "get": {
        "operationId": "getGraph",
        "tags": ["Recall"],
        "summary": "Entity/edge graph view",
        "description": "Return the tenant's entity/edge graph as nodes and edges, built from active text-object facts. Optionally focus on a query string plus its one-hop neighborhood and cap the node count.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Focus the graph on matches (name/predicate/object) plus their one-hop neighborhood.",
            "schema": { "type": "string" }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Keep the top-N nodes by degree (with induced edges). 0 (default) returns the full graph; focused views are capped at 80.",
            "schema": { "type": "integer", "default": 0 }
          }
        ],
        "responses": {
          "200": {
            "description": "Graph view.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Graph" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/walk": {
      "get": {
        "operationId": "walkGraph",
        "tags": ["Recall"],
        "summary": "BFS graph walk",
        "description": "Breadth-first graph walk from a start entity over fact edges, with depth, direction, and edge-type filters, optionally time-pinned with as_of. Tenant-scoped: the BFS never traverses another tenant's edges.",
        "parameters": [
          {
            "name": "start",
            "in": "query",
            "required": true,
            "description": "Start entity ref for the walk (e.g. \"Person:sarah\").",
            "schema": { "type": "string" }
          },
          {
            "name": "depth",
            "in": "query",
            "required": false,
            "description": "Maximum BFS depth (number of hops).",
            "schema": { "type": "integer" }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "description": "Edge traversal direction.",
            "schema": {
              "type": "string",
              "enum": ["out", "in", "both"]
            }
          },
          {
            "name": "edge_types",
            "in": "query",
            "required": false,
            "description": "Comma-separated predicate names to restrict which edges are followed.",
            "schema": { "type": "string" }
          },
          {
            "name": "as_of",
            "in": "query",
            "required": false,
            "description": "Time-travel instant in unix milliseconds.",
            "schema": { "type": "integer", "format": "int64" }
          }
        ],
        "responses": {
          "200": {
            "description": "Reached nodes and traversed edges.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WalkResult" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/connectors": {
      "get": {
        "operationId": "listConnectors",
        "tags": ["Connectors"],
        "summary": "List source connectors",
        "description": "List the tenant's configured source connectors, with their kind, target subject mapping, and a live ingested-fact count.",
        "responses": {
          "200": {
            "description": "Configured connectors.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ConnectorList" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "post": {
        "operationId": "registerConnector",
        "tags": ["Connectors"],
        "summary": "Register a source connector",
        "description": "Register or update a tenant-scoped source connector that ingests real data and digests it into provenance-bearing facts.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ConnectorInput" },
              "example": {
                "name": "hr-feed",
                "kind": "webhook",
                "label": "HR feed",
                "url": "https://example.com/hr/export.jsonl"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The registered connector.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Connector" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/connectors/{name}/events": {
      "post": {
        "operationId": "ingestConnectorEvents",
        "tags": ["Connectors"],
        "summary": "Webhook ingest",
        "description": "Inbound webhook delivery: push one event, or a JSON array / JSONL batch, in the request body. Each record is digested into provenance-bearing facts stamped with the connector as their source.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "Connector name to attribute the ingested facts to.",
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "A single source event object, or a JSON array of events.",
                "oneOf": [
                  { "type": "object", "additionalProperties": true },
                  {
                    "type": "array",
                    "items": { "type": "object", "additionalProperties": true }
                  }
                ]
              }
            },
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "description": "Newline-delimited JSON (JSONL): one event object per line."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ingest receipt: counts of records digested into facts.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/IngestReceipt" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/connectors/{name}/pull": {
      "post": {
        "operationId": "pullConnector",
        "tags": ["Connectors"],
        "summary": "Trigger a pull sync",
        "description": "Trigger a pull-mode sync: abagraph fetches the connector's configured URL (SSRF-guarded) and digests the result into provenance-bearing facts.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "Connector name to sync.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Ingest receipt: counts of records digested into facts.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/IngestReceipt" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/alerts": {
      "get": {
        "operationId": "listAlerts",
        "tags": ["Monitoring"],
        "summary": "List trust alerts",
        "description": "List the tenant's open and acknowledged trust alerts (monitoring on confidence, freshness, drift, and cost). Alerts are stored as facts, so acking supersedes open->acked and the prior state survives as bitemporal history.",
        "responses": {
          "200": {
            "description": "Trust alerts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/Alert" }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/reports": {
      "get": {
        "operationId": "listReports",
        "tags": ["Monitoring"],
        "summary": "List reports",
        "description": "List the tenant's scheduled/composable reports. Reports are composed of blocks that execute against real data when run.",
        "responses": {
          "200": {
            "description": "Reports.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/Report" }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/digest": {
      "post": {
        "operationId": "digest",
        "tags": ["Ingestion"],
        "summary": "Digest content into facts",
        "description": "Digest a piece of content into extracted, provenance-bearing facts using a caller-supplied (BYOK) extractor and judge. Facts below the confidence threshold land as candidates. PII masking is applied to text (early-access: partial, not a complete redaction guarantee). Returns a migration receipt.",
        "parameters": [ { "$ref": "#/components/parameters/IdempotencyKey" } ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/DigestRequest" },
              "example": {
                "content": "Sarah joined Acme as a staff engineer in 2024.",
                "content_type": "text/markdown",
                "source": "onboarding-doc"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Digest receipt: extracted candidate/active facts and counts.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/IngestReceipt" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/stream": {
      "get": {
        "operationId": "streamEvents",
        "tags": ["Streaming"],
        "summary": "Live change feed (SSE)",
        "description": "Server-Sent Events change feed. Holds the connection open and streams one SSE frame per committed write (assert/retract) as it lands, tenant-scoped so a subscriber only sees its own tenant's facts. Each event's `data` is a JSON object `{ \"op\": \"assert\"|\"retract\", \"tx\": <write-transaction number>, \"fact\": <Fact> }`. The server emits a `:keepalive` comment line roughly every 15 seconds while idle. This is abagraph's streaming surface.",
        "responses": {
          "200": {
            "description": "An open Server-Sent Events stream of change frames.",
            "headers": {
              "Cache-Control": {
                "description": "Always no-cache for the live stream.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "An SSE byte stream. Each frame is `data: <json>\\n\\n` where <json> is a StreamEvent; idle keepalives are `:keepalive\\n\\n` comment lines."
                },
                "example": "data: {\"op\":\"assert\",\"tx\":42,\"fact\":{\"id\":\"3690f770-baa2-4ce6-83da-c556e04ad9c0\",\"subject\":\"Person:sarah\",\"predicate\":\"works_at\",\"object\":\"Org:acme\",\"confidence\":0.95,\"source\":\"hr-feed\",\"status\":\"active\",\"valid_from\":1730000000000,\"valid_until\":null,\"recorded_at\":1730000000123}}\n\n"
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/ask": {
      "post": {
        "operationId": "ask",
        "tags": ["Ask"],
        "summary": "Natural-language ask over memory",
        "description": "Answer a natural-language question grounded in tenant-scoped evidence, using a caller-supplied (BYOK) model. Evidence is gathered from the engine (semantic recall); the supplied key is used only for the outbound model call and is never persisted or logged. Returns the answer plus the evidence facts and a reliability estimate.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/AskRequest" },
              "example": { "question": "where does sarah work?", "top_k": 8 }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Grounded answer with supporting evidence.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AskResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcpRpc",
        "tags": ["MCP"],
        "summary": "MCP endpoint (Streamable HTTP)",
        "description": "Model Context Protocol over the Streamable HTTP transport (JSON-RPC 2.0). One request in, one application/json response out (the spec permits a single JSON reply in place of an SSE stream). Methods: initialize, tools/list, tools/call. Advertised at /.well-known/mcp.json. Gated by the same bearer auth as REST; a scoped token is pinned to its namespace (the engine tenant). Tools: digest, remember, recall, recall_as_of, whats_changed, build_context, explain_fact. Prefer build_context at task start.",
        "parameters": [ { "$ref": "#/components/parameters/IdempotencyKey" } ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/JsonRpcRequest" },
              "example": {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "tools/call",
                "params": {
                  "name": "build_context",
                  "arguments": {
                    "task": "brief me on Sarah before a 1:1",
                    "namespace": "demo",
                    "seeds": ["Person:sarah"],
                    "max_tokens": 80
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A JSON-RPC 2.0 response envelope.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JsonRpcResponse" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/session": {
      "post": {
        "operationId": "createSession",
        "tags": ["Auth"],
        "summary": "Exchange a key for a session cookie",
        "description": "Validate a tenant or admin key and set an HttpOnly `abagraph_session` cookie carrying the same scope. This is the login door used by the browser console, so it is the one endpoint that does not itself require a bearer token. The key is re-validated on every subsequent request, like a bearer token.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SessionRequest" },
              "example": { "key": "YOUR_TENANT_KEY" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Logged in. Sets an HttpOnly abagraph_session cookie.",
            "headers": {
              "Set-Cookie": {
                "description": "HttpOnly, Secure, SameSite=Lax session cookie carrying the key's scope.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SessionResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "delete": {
        "operationId": "clearSession",
        "tags": ["Auth"],
        "summary": "Clear the session cookie",
        "description": "Log out by clearing the abagraph_session cookie.",
        "security": [],
        "responses": {
          "200": {
            "description": "Logged out. Expires the abagraph_session cookie.",
            "headers": {
              "Set-Cookie": {
                "description": "Expired (Max-Age=0) abagraph_session cookie.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Per-tenant API key; subdomain-bound. Send `Authorization: Bearer <key>`. A tenant key authenticates only on its own tenant subdomain (e.g. nas.abagraph.com); an admin token bypasses tenant scope. Browser clients may instead carry the HttpOnly session cookie set by POST /api/session. By design, an unauthenticated or cross-tenant request to a gated surface returns 404 (non-disclosure) rather than 401/403, so the protected surface is never advertised. Keys are provisioned via early-access onboarding at https://abagraph.com (no public self-serve OAuth flow yet)."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Client-generated token (e.g. a UUID) that makes a mutating POST safe to retry. The first request with a given key is processed and its response cached; any later request carrying the same key on the same route returns the original response verbatim with an `Idempotency-Replayed: true` header instead of applying the write again. Scope is the token's tenant; keys live for the server process lifetime.",
        "schema": { "type": "string", "maxLength": 255 },
        "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      }
    },
    "responses": {
      "NotFound": {
        "description": "Not found, or the non-disclosure response for an unauthenticated or cross-tenant request. abagraph returns 404 (rather than 401/403) for gated surfaces so a token-protected resource is never advertised.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "NotFound", "message": "not found" } }
          }
        }
      },
      "BadRequest": {
        "description": "Invalid request body or parameters (e.g. SchemaInvalid), or a rejected write (e.g. Conflict on a strict predicate).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "SchemaInvalid", "message": "subject is required" } }
          }
        }
      },
      "Unauthorized": {
        "description": "Invalid key supplied to the session login endpoint.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "Unauthorized", "message": "invalid key" } }
          }
        }
      }
    },
    "schemas": {
      "Fact": {
        "type": "object",
        "description": "A single bitemporal, source-backed subject-predicate-object claim. Facts are immutable once written; you never UPDATE, you assert a new fact and abagraph closes (supersedes) the contradicting one. The history IS the data.",
        "required": ["subject", "predicate", "object", "confidence", "status", "valid_from"],
        "properties": {
          "id": {
            "type": "string",
            "description": "Server-assigned fact id (UUID)."
          },
          "subject": {
            "type": "string",
            "description": "Entity the claim is about, conventionally \"Type:id\" (e.g. \"Person:sarah\")."
          },
          "predicate": {
            "type": "string",
            "description": "Relation or attribute name (e.g. \"works_at\")."
          },
          "object": {
            "description": "Value of the relation: an entity ref or text string, number, boolean, null, or a blob reference."
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Calibrated confidence in [0.0, 1.0]. Below the candidate threshold (default 0.75) the fact is stored with status candidate and supersedes nothing."
          },
          "source": {
            "type": ["string", "null"],
            "description": "Provenance: where the fact came from (e.g. \"hr-feed\", a connector name)."
          },
          "status": {
            "type": "string",
            "enum": ["active", "candidate", "superseded", "retracted"],
            "description": "Lifecycle status. active: currently valid, visible in default queries, participates in conflict resolution. candidate: below confidence threshold, visible but supersedes nothing. superseded: closed by a newer conflicting fact, visible only via as_of. retracted: explicitly retracted, hidden unless include_retracted is set."
          },
          "valid_from": {
            "type": "integer",
            "format": "int64",
            "description": "Valid-time start (unix milliseconds): when the claim becomes true in the modeled world."
          },
          "valid_until": {
            "type": ["integer", "null"],
            "format": "int64",
            "description": "Valid-time end (unix milliseconds), or null while the claim is still open-ended."
          },
          "recorded_at": {
            "type": "integer",
            "format": "int64",
            "description": "Transaction/record time (unix milliseconds): when abagraph learned the fact."
          },
          "properties": {
            "type": ["object", "null"],
            "additionalProperties": true,
            "description": "Optional JSON sidecar of extra structured properties."
          },
          "tenant": {
            "type": ["string", "null"],
            "description": "Tenant the fact belongs to; absent for global/admin-owned facts."
          },
          "tx": {
            "type": "integer",
            "format": "int64",
            "description": "Monotonic write-transaction number stamped on every fact a write touches; cursor consumers replay with tx > last_seen."
          }
        }
      },
      "FactInput": {
        "type": "object",
        "description": "Body for asserting a fact. Only subject and predicate are required; the tenant is stamped from the auth context, not the body.",
        "required": ["subject", "predicate"],
        "properties": {
          "subject": {
            "type": "string",
            "description": "Entity the claim is about, conventionally \"Type:id\"."
          },
          "predicate": {
            "type": "string",
            "description": "Relation or attribute name."
          },
          "object": {
            "description": "Value of the relation: an entity ref or text string, number, boolean, null, or blob reference."
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Calibrated confidence in [0.0, 1.0]. Defaults high; below the candidate threshold (default 0.75) the fact lands as a candidate."
          },
          "source": {
            "type": "string",
            "description": "Provenance label for the fact."
          },
          "valid_from": {
            "type": "integer",
            "format": "int64",
            "description": "Valid-time start (unix milliseconds). Defaults to now."
          },
          "valid_until": {
            "type": ["integer", "null"],
            "format": "int64",
            "description": "Valid-time end (unix milliseconds), or null for open-ended."
          },
          "properties": {
            "type": "object",
            "additionalProperties": true,
            "description": "Optional JSON sidecar of extra structured properties."
          }
        }
      },
      "ContextRequest": {
        "type": "object",
        "description": "Inputs for building a context packet.",
        "properties": {
          "seeds": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Seed entity refs to anchor the packet (e.g. [\"Person:sarah\"])."
          },
          "goal": {
            "type": "string",
            "description": "Natural-language description of the agent task; used to focus and label the packet."
          },
          "token_budget": {
            "type": "integer",
            "description": "Token budget for fact selection. When set, facts are ranked by confidence/recency and trimmed to fit; when omitted, the max_facts count cap applies in collection order."
          },
          "project": {
            "type": ["string", "null"],
            "description": "Optional project entity ref to scope the packet."
          },
          "max_facts": {
            "type": "integer",
            "default": 64,
            "description": "Cap on the number of facts when no token_budget is given."
          },
          "max_pages": {
            "type": "integer",
            "default": 8,
            "description": "Cap on adjacent narrative page snippets."
          },
          "as_of": {
            "type": ["integer", "null"],
            "format": "int64",
            "description": "Time-travel instant in unix milliseconds: build the packet as the memory stood at this time."
          }
        }
      },
      "ContextPacket": {
        "type": "object",
        "description": "A compact, deterministic, source-backed bundle for an agent goal: ranked facts plus a one-hop graph walk plus adjacent narrative snippets, fused under a token budget with conflicts surfaced. The recommended way to load memory at the start of a task.",
        "properties": {
          "summary": {
            "type": "string",
            "description": "One-line summary of the packet's focus and counts."
          },
          "entities": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Focus entity refs: the seeds plus entities pulled in by the fact lookup and one-hop walk."
          },
          "facts": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Fact" },
            "description": "Ranked, source-backed facts selected under the token budget."
          },
          "pages": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/PageSnippet" },
            "description": "Adjacent narrative snippets for the focus entities."
          },
          "tokens": {
            "type": "integer",
            "description": "Estimated tokens of the selected facts (the portion that fit the budget)."
          },
          "total_tokens": {
            "type": "integer",
            "description": "Estimated tokens of all candidate facts before budget trimming; total_tokens - tokens is the reduction the budget achieved."
          },
          "warnings": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ContextWarning" },
            "description": "Surfaced issues such as conflicts among the selected facts."
          },
          "kind": {
            "type": "string",
            "description": "Response discriminator; always \"context_packet\"."
          }
        }
      },
      "PageSnippet": {
        "type": "object",
        "description": "An adjacent narrative snippet attached to a focus entity.",
        "properties": {
          "id": { "type": "string", "description": "Snippet id." },
          "subject": { "type": "string", "description": "Entity ref the snippet is attached to." },
          "title": { "type": "string", "description": "Snippet title." },
          "body": { "type": "string", "description": "Snippet text." },
          "source": { "type": ["string", "null"], "description": "Provenance of the snippet." }
        }
      },
      "ContextWarning": {
        "type": "object",
        "description": "A surfaced issue in a context packet, such as a conflict.",
        "properties": {
          "kind": { "type": "string", "description": "Warning category (e.g. \"conflict\")." },
          "message": { "type": "string", "description": "Human-readable detail." }
        }
      },
      "SearchRequest": {
        "type": "object",
        "description": "Body for semantic recall.",
        "properties": {
          "question": { "type": "string", "description": "Natural-language query to embed and match." },
          "top_k": { "type": "integer", "description": "Maximum number of facts to return." },
          "as_of": { "type": "integer", "format": "int64", "description": "Time-travel instant in unix milliseconds." }
        }
      },
      "SearchResult": {
        "type": "object",
        "description": "Ranked semantic recall result.",
        "properties": {
          "kind": { "type": "string", "description": "Always \"search_result\"." },
          "question": { "type": "string", "description": "The echoed query." },
          "answer_facts": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Fact" },
            "description": "Facts ranked by cosine similarity to the query."
          },
          "entities": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Entity refs appearing in the answer facts."
          }
        }
      },
      "Graph": {
        "type": "object",
        "description": "An entity/edge graph view.",
        "properties": {
          "nodes": {
            "type": "array",
            "items": { "type": "object", "additionalProperties": true },
            "description": "Graph nodes (entities)."
          },
          "edges": {
            "type": "array",
            "items": { "type": "object", "additionalProperties": true },
            "description": "Graph edges (facts with entity-ref objects)."
          },
          "total_nodes": { "type": "integer", "description": "Total nodes before any limit was applied." },
          "total_edges": { "type": "integer", "description": "Total edges before any limit was applied." }
        }
      },
      "WalkResult": {
        "type": "object",
        "description": "Result of a BFS graph walk.",
        "properties": {
          "reached": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Entity refs reached by the walk."
          },
          "edges": {
            "type": "array",
            "items": { "type": "object", "additionalProperties": true },
            "description": "Edges traversed during the walk."
          }
        }
      },
      "ConnectorInput": {
        "type": "object",
        "description": "Body for registering a source connector.",
        "required": ["name"],
        "properties": {
          "name": { "type": "string", "description": "Unique connector name within the tenant." },
          "kind": { "type": "string", "description": "Connector kind (e.g. webhook, pull source type)." },
          "label": { "type": "string", "description": "Human-readable label." },
          "url": { "type": ["string", "null"], "description": "Source URL fetched by pull-mode syncs (SSRF-guarded)." }
        }
      },
      "Connector": {
        "type": "object",
        "description": "A configured source connector.",
        "properties": {
          "name": { "type": "string", "description": "Connector name." },
          "kind": { "type": "string", "description": "Connector kind." },
          "label": { "type": "string", "description": "Human-readable label." },
          "url": { "type": ["string", "null"], "description": "Configured source URL, if any." },
          "subject": { "type": "string", "description": "Target subject mapping for ingested facts." },
          "ingested": { "type": "integer", "description": "Live count of active + held candidate facts attributed to this connector." }
        }
      },
      "ConnectorList": {
        "type": "object",
        "description": "List of configured connectors.",
        "properties": {
          "connectors": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Connector" }
          }
        }
      },
      "IngestReceipt": {
        "type": "object",
        "description": "Receipt from a digest/ingest operation: how many records were extracted and how they landed.",
        "additionalProperties": true,
        "properties": {
          "asserted": { "type": "integer", "description": "Facts asserted as active." },
          "candidates": { "type": "integer", "description": "Facts held as candidates (below the confidence threshold)." },
          "records": { "type": "integer", "description": "Source records processed." }
        }
      },
      "Alert": {
        "type": "object",
        "description": "A trust alert (monitoring on confidence, freshness, drift, cost). Stored as a fact, so its state transitions are bitemporal history.",
        "additionalProperties": true,
        "properties": {
          "subject": { "type": "string", "description": "Alert subject, conventionally \"Alert:<id>\"." },
          "object": { "type": "string", "description": "Alert state, e.g. \"open\" or \"acked\"." }
        }
      },
      "Report": {
        "type": "object",
        "description": "A composable/scheduled report. Stored as a tenant-scoped fact; running it executes its blocks against real data.",
        "additionalProperties": true,
        "properties": {
          "subject": { "type": "string", "description": "Report subject, conventionally \"Report:<id>\"." }
        }
      },
      "DigestRequest": {
        "type": "object",
        "description": "Body for digesting content into facts.",
        "required": ["content"],
        "properties": {
          "content": { "type": "string", "description": "The raw content to digest." },
          "content_type": { "type": "string", "default": "text/markdown", "description": "MIME type of the content." },
          "source": { "type": "string", "default": "upload", "description": "Provenance label stamped on the extracted facts." },
          "threshold": { "type": "number", "minimum": 0, "maximum": 1, "description": "Confidence threshold below which extracted facts land as candidates (default 0.75)." },
          "mask": {
            "type": "string",
            "enum": ["redact", "hash", "hash_sha256", "shift_date"],
            "default": "redact",
            "description": "PII masking strategy applied to text (early-access: partial, not a complete redaction guarantee)."
          },
          "extract_model": { "type": "object", "additionalProperties": true, "description": "Caller-supplied (BYOK) extractor model config; the key is used only for the model call and never persisted or logged." },
          "judge_model": { "type": "object", "additionalProperties": true, "description": "Caller-supplied (BYOK) judge model config." },
          "model": { "type": "object", "additionalProperties": true, "description": "Fallback model config used when extract_model/judge_model are omitted." }
        }
      },
      "AskRequest": {
        "type": "object",
        "description": "Body for a natural-language ask.",
        "required": ["question"],
        "properties": {
          "question": { "type": "string", "description": "The natural-language question." },
          "top_k": { "type": "integer", "default": 8, "description": "Number of evidence facts to gather." },
          "model": { "type": "object", "additionalProperties": true, "description": "Caller-supplied (BYOK) model config; the key is used only for the outbound call and never persisted or logged." }
        }
      },
      "AskResponse": {
        "type": "object",
        "description": "A grounded answer with supporting evidence.",
        "properties": {
          "answer": { "type": "string", "description": "The model's answer, grounded in the evidence facts." },
          "evidence": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Fact" },
            "description": "The facts used as evidence."
          },
          "model_confidence": { "type": ["number", "null"], "description": "The model's self-reported confidence, if any." },
          "system_reliability": { "type": ["number", "null"], "description": "Mean confidence of the evidence facts." },
          "tokens": {
            "type": "object",
            "description": "Token usage for the model call.",
            "properties": {
              "in": { "type": "integer", "description": "Input tokens." },
              "out": { "type": "integer", "description": "Output tokens." }
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "description": "Liveness response.",
        "properties": {
          "ok": { "type": "boolean", "description": "True when the service is live." }
        }
      },
      "Stats": {
        "type": "object",
        "description": "Aggregate counts for the tenant.",
        "properties": {
          "facts": {
            "type": "object",
            "description": "Fact counts by status.",
            "properties": {
              "active": { "type": "integer" },
              "candidate": { "type": "integer" },
              "superseded": { "type": "integer" },
              "retracted": { "type": "integer" },
              "total": { "type": "integer" }
            }
          },
          "entities": { "type": "integer", "description": "Distinct subjects." },
          "predicates": { "type": "integer", "description": "Distinct predicates." },
          "tenants": { "type": "integer", "description": "Distinct tenants (admin) or 1 (tenant scope)." },
          "tenant": { "type": "string", "description": "The caller's tenant, or \"admin\"." },
          "version": { "type": "string", "description": "Engine version." }
        }
      },
      "SessionRequest": {
        "type": "object",
        "description": "Login body.",
        "required": ["key"],
        "properties": {
          "key": { "type": "string", "description": "The tenant or admin key." }
        }
      },
      "SessionResponse": {
        "type": "object",
        "description": "Login result describing the granted scope.",
        "properties": {
          "scope": {
            "type": "string",
            "enum": ["admin", "tenant", "open"],
            "description": "Granted scope."
          },
          "tenant": { "type": "string", "description": "The tenant, when scope is tenant." }
        }
      },
      "JsonRpcRequest": {
        "type": "object",
        "description": "A JSON-RPC 2.0 request for the MCP endpoint.",
        "required": ["jsonrpc", "method"],
        "properties": {
          "jsonrpc": { "type": "string", "const": "2.0", "description": "Always \"2.0\"." },
          "id": { "description": "Request id (number or string); omit for notifications." },
          "method": {
            "type": "string",
            "enum": ["initialize", "tools/list", "tools/call"],
            "description": "JSON-RPC method."
          },
          "params": {
            "type": "object",
            "additionalProperties": true,
            "description": "Method params. For tools/call: { name, arguments }. Tool names: digest, remember, recall, recall_as_of, whats_changed, build_context, explain_fact."
          }
        }
      },
      "JsonRpcResponse": {
        "type": "object",
        "description": "A JSON-RPC 2.0 response envelope.",
        "properties": {
          "jsonrpc": { "type": "string", "const": "2.0" },
          "id": { "description": "Echoed request id." },
          "result": { "description": "Method result, present on success." },
          "error": {
            "type": "object",
            "description": "Present on failure.",
            "properties": {
              "code": { "type": "integer" },
              "message": { "type": "string" }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "Standard error envelope.",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Machine-readable error code (e.g. Conflict, NotFound, SchemaInvalid, Unauthorized, MethodNotAllowed)."
              },
              "message": {
                "type": "string",
                "description": "Human-readable detail."
              }
            }
          }
        }
      }
    }
  }
}
