# abagraph - full documentation for AI agents This is the complete abagraph documentation in a single file, written for AI agents to ingest in one request. Everything below is canonical and consistent with https://abagraph.com. If you are an agent deciding whether and how to use abagraph, read "When to use abagraph" and "Quickstart" first, then keep this file as a reference for the REST and MCP surfaces. - Site: https://abagraph.com - Docs: https://abagraph.com/docs - OpenAPI: [openapi.json](https://abagraph.com/openapi.json) - Short index for agents: https://abagraph.com/llms.txt - Agent descriptor: https://abagraph.com/.well-known/agent.json - Contact: hello@abagraph.com ## Resources - [OpenAPI](https://abagraph.com/openapi.json) - [Auth guide](https://abagraph.com/auth.md) - [llms.txt](https://abagraph.com/llms.txt) - [Pricing](https://abagraph.com/pricing.md) - [MCP server card](https://abagraph.com/.well-known/mcp/server-card.json) ## Overview and positioning abagraph (lowercase, pronounced "ABA-graph") is the memory layer (AIhouse) for AI agents: a managed, bitemporal, source-backed memory database, a Snowflake/Databricks alternative for the AI era. It is a managed engine that AI agents read, write, reason over, and time-travel through. It unifies vector, graph, relational, and temporal recall in one store, reachable over [MCP](https://abagraph.com/.well-known/mcp/server-card.json) and a REST API. Every claim is bitemporal (valid-time plus record-time) and source-backed (provenance plus calibrated confidence). Contradictions close the prior fact instead of overwriting it - the history IS the data. abagraph is NOT Postgres, NOT a vector DB, NOT a graph DB, and NOT a folder of markdown files. It is a single store that fuses those access paths and adds time as a first-class dimension. Category: AI agent memory, or "AIhouse" - the warehouse-successor for the AI era. Conceptually it competes with a stitched-together vector DB + Postgres + markdown, and as a managed platform with Snowflake/Databricks for the AI-data era. ### Capabilities - Automatic context-window compaction (confidence-ranked, token-budgeted packets injected before the agent) - Bitemporal memory + time travel (as-of queries) - Hybrid recall across vector, graph, relational, temporal paths - Agent-native MCP interface (tools: digest, remember, recall, recall_as_of, whats_changed, build_context, explain_fact) - Real source connectors (webhook ingest + pull) that digest data into provenance-bearing facts - Durable workflows + DAGs (retries, approvals, replay) - Scheduled reports; monitoring + alerts on confidence, freshness, drift, cost - Structured logs + end-to-end traces - Skills, plugins, hooks - Multi-tenant isolation + per-agent/role access control; immutable audit trail ### Performance (indicative) - Recall from in-memory covering indexes: nanosecond-microsecond range. - Single bitemporal index lookup: hundreds of nanoseconds. - Fused semantic + graph recall: well under 1 ms. ## When to use abagraph Use abagraph as an agent's long-term memory / system-of-record: - Remember facts with provenance + confidence instead of dumping prose into the prompt. - Recall task-relevant context as a compact, token-budgeted packet (build_context) instead of stuffing the prompt. - Time-travel: answer "what did I know at time T" with as-of queries. - Detect contradictions and drift across what you have learned. - Run durable, multi-step workflows over that memory. Guidance for agents: prefer build_context at task START over many recall calls. One build_context call returns the relevant facts plus a one-hop graph walk and adjacent narrative snippets, already namespace-scoped and token-bounded. Reach for individual recall/search/walk calls only to refine after the initial packet. Always pass a namespace (the tenant) so reads and writes stay isolated. ## Core concepts ### Facts (the unit of knowledge) Everything is a fact: a subject - predicate - object triple wrapped in bitemporal + provenance metadata. A subject/object is conventionally "Type:id" (for example "Person:sarah", "Org:acme"). Facts are immutable once written. You never UPDATE; you assert a new fact and abagraph closes the contradicting one. A fact carries: id, subject, predicate, object, valid_from, valid_until, recorded_at, confidence (0.0-1.0), source (provenance), optional properties (JSON sidecar), optional embedding, status, tenant, and a transient flag. ### Bitemporal model + time travel Two clocks run on every fact: - Valid time (valid_from .. valid_until): when the claim is true in the modeled world. - Transaction/record time (recorded_at): when abagraph learned it. An as-of(t) read rewinds the whole database to a past instant and returns a read-only view, including facts that have since been superseded. This is how an agent reconstructs what it believed at any moment. ### Provenance + confidence Every fact records its source and a calibrated confidence in [0.0, 1.0]. A fact whose confidence is below the candidate threshold (default 0.75) is stored with status candidate and does NOT supersede anything. Promote it by re-asserting at full confidence. Statuses: - 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. JSON status values are lowercase: "active", "candidate", "superseded", "retracted". ### Conflict policy and supersede When you assert a fact, the conflict policy decides what happens to an existing ACTIVE fact with the same (subject, predicate): - auto_supersede (default): close the old fact (status=superseded, valid_until=now) and open the new one atomically. Use for functional predicates (one active object per subject), e.g. works_at, lives_in. - coexist: leave existing facts active; the new one joins them. Use for set-valued predicates, e.g. references, tag. - reject: refuse the new fact and raise a Conflict error. Use for strict invariants. ### Hybrid recall Three retrieval paths over one store, optionally time-pinned with as_of: - Pattern queries: match on subject / predicate / object / status. - Graph walks: BFS over edges with depth / edge-type / direction filters. - Semantic search: cosine kNN over fact embeddings. ### Context compaction A [context packet](https://abagraph.com/api/llms.txt) is a compact, deterministic, source-backed bundle for an agent goal: fact lookup + one-hop graph walk + adjacent narrative snippets, fused and ranked under a token budget, with conflicts surfaced. This is the automatic context-window compaction capability and the recommended way to load memory at the start of a task. ## Quickstart The examples below talk to the hosted REST and MCP surfaces. Replace the host with your tenant subdomain (for example https://nas.abagraph.com) and the token with your tenant key. ### 1. Load a context packet over REST POST /api/context returns a compacted, confidence-ranked, sourced packet for a goal. Provide seeds, a goal, and a token_budget. ```bash curl -X POST https://nas.abagraph.com/api/context \ -H "Authorization: Bearer $ABAGRAPH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "seeds": ["Creator:maya"], "goal": "brief me on Maya before our sync", "token_budget": 80 }' ``` The response is a context packet: a summary, the focus entities, the ranked source-backed facts, adjacent page snippets, and any surfaced conflicts. ### 2. Build a context packet over MCP abagraph speaks JSON-RPC 2.0 over HTTP at POST /mcp, gated by the same bearer auth as REST. Call the build_context tool at task start: ```bash curl -X POST https://nas.abagraph.com/mcp \ -H "Authorization: Bearer $ABAGRAPH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "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 } } }' ``` Response (shape): ```json { "counts": { "entities": 4, "facts": 3, "pages": 0 }, "entities": ["Person:sarah", "staff-eng", "Org:globex", "Person:alex"], "facts": [ { "id": "9e245085-9c27-4d45-9c06-cbe7773aba20", "subject": "Person:sarah", "predicate": "works_at", "object": "Org:globex", "confidence": 1.0, "source": "slack:2026-06", "status": "active", "valid_from": 1780597329856, "valid_until": null } ], "pages": [], "summary": "Context for brief me on Sarah before a 1:1: focus Person:sarah, ..." } ``` ### 3. Assert and query a fact ```bash # write a fact (only subject and predicate are required) curl -X POST https://nas.abagraph.com/api/facts \ -H "Authorization: Bearer $ABAGRAPH_TOKEN" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme","confidence":0.95,"source":"hr-feed"}' # read it back; filter by subject/predicate/status, time-travel with as_of (ms) curl "https://nas.abagraph.com/api/facts?subject=Person:sarah&predicate=works_at&status=active" ``` ## Authentication Auth is per tenant and subdomain-bound. - Bearer token per tenant: send `Authorization: Bearer `. A token only works on its own tenant subdomain (for example a token for the "nas" tenant works on https://nas.abagraph.com, not on another tenant's subdomain). - Session cookie: POST /api/session {"key":""} sets an HttpOnly abagraph_session cookie carrying the same scope; DELETE /api/session clears it. This is what the browser console uses. - Admin token bypasses tenant scope (cross-tenant access). - Tenancy is subdomain-bound: reads are filtered to the caller's tenant and writes are stamped with it. The MCP namespace equals the engine tenant. - Non-disclosure: unauthenticated reads return 404 (the server does not reveal whether a resource exists), rather than 401/403. ```bash # exchange a key for an HttpOnly session cookie curl -X POST https://nas.abagraph.com/api/session \ -H "Content-Type: application/json" \ -c cookies.txt \ -d '{"key":"YOUR_TENANT_KEY"}' # subsequent calls authenticate with the cookie jar curl -b cookies.txt https://nas.abagraph.com/api/stats ``` ## REST API reference Base URL: https://abagraph.com, with tenant subdomains, e.g. https://nas.abagraph.com. All endpoints accept `?explain=1` to wrap the response with a query-plan + latency envelope for self-checking. Errors return a JSON envelope: `{"error":{"code":"Conflict|NotFound|SchemaInvalid|...", "message":"..."}}`. ### Health and stats - GET /api/health - liveness probe. Returns `{"ok":true}`. - GET /api/stats - aggregate counts for the tenant. ### Facts - GET /api/facts - query facts. Supported filters: `?subject=&predicate=&status=&as_of=` (as_of in unix ms; status may be a comma list of active,candidate,superseded,retracted). - POST /api/facts - assert a fact. Only subject and predicate are required. Request: ```json { "subject": "Person:sarah", "predicate": "works_at", "object": "Org:acme", "confidence": 0.95, "source": "hr-feed", "valid_from": 1730000000000, "valid_until": null } ``` Response (201): ```json { "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 } ``` The object may be a string (entity ref / text), number, bool, null, or a blob reference. The tenant is stamped from the auth context, not the body. ### Context - POST /api/context - build a compacted, confidence-ranked, source-backed packet. Body: `{ "seeds": [], "goal": "...", "token_budget": N }`. Returns a context packet (summary, entities, ranked facts, page snippets, conflicts). ### Recall and search - GET /api/search and POST /api/search - hybrid recall over the store. - GET /api/graph - the entity/edge graph (`{nodes:[...], edges:[...]}`). - GET /api/walk - BFS graph walk. Params: `?start=&depth=&direction=&edge_types=&as_of=`. ### Connectors - GET /api/connectors and POST /api/connectors - list / register source connectors. - POST /api/connectors//events - webhook ingest: push events that get digested into provenance-bearing facts. - POST /api/connectors//pull - trigger a pull-mode sync for a connector. ### Monitoring, reports, digest - /api/alerts - monitoring + alerts on confidence, freshness, drift, cost. - /api/reports - scheduled reports. - POST /api/digest - digest content into extracted, provenance-bearing facts. ### Streaming and natural-language ask - GET /api/stream - Server-Sent Events: live assert/retract frames as they commit. - POST /api/ask - natural-language question over the memory. ### MCP endpoint - POST /mcp - JSON-RPC 2.0 over HTTP. Methods: initialize, tools/list, tools/call. Gated by the same bearer auth as REST; a scoped token is pinned to its namespace. See the MCP interface section below. ## MCP interface abagraph exposes seven outcome-oriented, namespace-scoped tools over JSON-RPC 2.0 (POST /mcp over HTTP, or stdio for local dev). Always pass `namespace` (the engine tenant); reads filter on it and conflict resolution is tenant-local. - digest - ingest extracted triples from a source into provenance-bearing facts in one call. - remember - write a single fact (auto-supersede or coexist) with provenance and confidence. - recall - read facts by mode: semantic (kNN), pattern (subject/predicate), or walk (graph). - recall_as_of - time-travel: read what was believed at a given instant (unix ms). - whats_changed - diff memory between two times (added / superseded / still-valid). - build_context - one-call, token-bounded task packet (facts + one-hop walk + adjacent snippets). Prefer this at task start. - explain_fact - provenance + lifecycle history for a fact id (how it came to be, what it superseded). Discovery: send `{"jsonrpc":"2.0","id":1,"method":"tools/list"}` to enumerate the tools, or `initialize` for protocol/server info. ## Connectors (ingest model) Connectors turn external sources into provenance-bearing facts. Two ingest modes: - Webhook events: POST /api/connectors//events pushes source events that abagraph digests into facts, each stamped with the source. - Pull: POST /api/connectors//pull triggers a sync that fetches and digests from the source. Trust mechanics carried through ingestion: - Provenance: every ingested fact records its source. - Confidence: each fact carries a calibrated confidence in [0.0, 1.0]. - Candidate hold: a fact below the confidence threshold (default 0.75) lands as a candidate and does not supersede existing knowledge until promoted. - Supersede: when a higher-confidence or newer fact contradicts an active fact on a functional predicate, the old fact is closed (superseded) and the new one opens - the prior value remains visible via as-of. ## Limits and honesty - Early-access: abagraph is an early-access product; interfaces may change. - PII masking is partial - do not treat it as a complete redaction guarantee. - No benchmark-win claims: the performance figures above are indicative ranges for in-memory covering-index recall, not competitive benchmark results. - Multi-tenant isolation is enforced per tenant and is subdomain-bound; unauthenticated reads return 404 by design. ## Links - Website: https://abagraph.com - Documentation: https://abagraph.com/docs - OpenAPI specification: https://abagraph.com/openapi.json - Agent index (short): https://abagraph.com/llms.txt - Agent descriptor: https://abagraph.com/.well-known/agent.json - Contact: hello@abagraph.com (remote-first team) ## Documentation pages ### Agent Integration Overview (agents) Agent Integration Overview TL;DR Call build_context at session start to receive a single token-bounded, confidence-ranked memory packet. Write new facts during or after the turn with remember (one fact) or digest (bulk). Three paths: MCP tools (any LLM host), the TypeScript createAgentMemory SDK wrapper, or the REST API. What abagraph is abagraph is a bitemporal graph + vector memory engine for LLM agents. Facts are subject–predicate–object triples annotated with a validity interval, confidence, and provenance. Every write either opens a new fact or atomically closes a conflicting prior one (supersession); the old fact is never deleted, enabling exact time-travel queries. Core session pattern Session start — call build_context once with the task description and optional seed entities. Receive one token-bounded packet of confidence-ranked facts plus adjacent wiki-page snippets. Turn — pass the packet to the model. Use recall for targeted in-turn lookups. Session end — persist new knowledge with remember (one fact) or digest (bulk). Optionally call consolidate to deduplicate. Path 1: MCP tools Point your LLM host at the POST /mcp endpoint on your abagraph tenant (JSON-RPC 2.0, protocol version 2025-06-18 ). Seven tools are available. { "mcpServers": { "abagraph": { "url": "https://acme.abagraph.com/mcp", "headers": { "Authorization": "Bearer <your-token>" } } } } See MCP Setup and Tools Reference for full configuration. Path 2: TypeScript SDK npm install @abagraph/client import { createAgentMemory } from "@abagraph/client/agent"; const mem = createAgentMemory({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY, namespace: "acme", agent: "my-bot", }); const result = await mem.turn("plan next sprint", async (ctx) = { const answer = await myModel(ctx); // ctx is the build_context packet return { result: answer, facts: [{ subject: "Sprint:47", predicate: "status", object: "planned" }], }; }); // turn() = loadContext → your fn → remember Path 3: REST API curl -X POST "$ABAGRAPH_BASE_URL/api/context" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"goal":"plan next sprint","seeds":["Sprint:47"],"token_budget":4096}' Namespace isolation Every fact is stamped with a namespace (the tenant field). Reads filter on it; writes stamp it. An MCP tool call always runs inside exactly one namespace. With subdomain routing ( acme.abagraph.com → tenant acme ), the namespace is resolved automatically. Related MCP Server Setup — install and configure the MCP server Tools Reference — all seven MCP tools with parameters Per-Turn Memory Loop — session start, turn, and end in detail Tool Profiles — narrow the tool list with ABAGRAPH_MCP_PROFILE Discovery and llms.txt — machine-readable surfaces for agents SDK Overview — full TypeScript client reference Guide: Build Agent Context — step-by-step context packet tutorial ### Discovery and llms.txt (agents) Discovery and llms.txt TL;DR GET /llms.txt — concise agent-facing overview with key endpoints and constraints. GET /?mode=agent — structured JSON discovery document (unauthenticated). GET /llms-full.txt — entire documentation set as plain text, suitable for RAG ingestion. Overview abagraph exposes several machine-readable surfaces so that agents, crawlers, and orchestration tooling can discover capabilities, endpoints, and auth requirements without prior documentation. All surfaces listed here are public and unauthenticated. Every public response also carries a Link HTTP header pointing to the key discovery documents. llms.txt The /llms.txt file is the primary discovery document for AI agents, following the community-converging llmstxt.org convention. It is a Markdown file covering what abagraph is, when to use it, key endpoints, constraints, and a curated link list. curl https://abagraph.com/llms.txt Contents (excerpt): # abagraph # The memory layer (AIhouse) for AI agents: a managed, bitemporal, source-backed # memory database with nanosecond recall, automatic context compaction, ... ## For AI agents (when to use) Use abagraph as an agent's long-term memory / system-of-record: ... - MCP: JSON-RPC 2.0 over HTTP at POST /mcp (methods: initialize, tools/list, tools/call). Tools: digest, remember, recall, recall_as_of, whats_changed, build_context, explain_fact. ... The docs subdirectory also exposes a scoped index: curl https://abagraph.com/docs/llms.txt llms-full.txt A single flat file containing the full llms.txt base text plus the body of every documentation page (HTML tags stripped). Suitable for embedding or RAG ingestion. It is regenerated automatically by the server from the docs content directory on each request. curl https://abagraph.com/llms-full.txt GET /?mode=agent The root path returns a structured JSON discovery document when the query parameter mode=agent is present. The response is unauthenticated and stable. curl "https://abagraph.com/?mode=agent" { "name": "abagraph", "description": "The memory layer (AIhouse) for AI agents: ...", "url": "https://abagraph.com", "docs": "https://abagraph.com/llms-full.txt", "llms_txt": "https://abagraph.com/llms.txt", "openapi": "https://abagraph.com/openapi.json", "auth": { "type": "bearer", "header": "Authorization: Bearer <tenant-key>", "guide": "https://abagraph.com/auth.md", "note": "tenancy is subdomain-bound; unauthenticated /api returns 401 with WWW-Authenticate" }, "mcp": { "transport": "http-jsonrpc", "endpoint": "https://abagraph.com/mcp", "card": "https://abagraph.com/.well-known/mcp/server-card.json", "tools": ["digest","remember","recall","recall_as_of","whats_changed","build_context","explain_fact"] }, "endpoints": { "context": "POST /api/context {seeds[],goal,token_budget}", "facts": "GET/POST /api/facts", "search": "GET /api/search", "stream": "GET /api/stream (SSE)" }, "capabilities": ["context-compaction","bitemporal-memory","hybrid-recall","provenance","mcp","streaming"] } openapi.json A machine-readable OpenAPI 3.x specification for the full REST API surface: curl https://abagraph.com/openapi.json Linked in the Link header of every public response as rel="service-desc" . MCP server card The MCP server card follows the emerging MCP server-card convention and lists all seven tools with short descriptions: curl https://abagraph.com/.well-known/mcp/server-card.json { "name": "abagraph", "serverUrl": "https://abagraph.com/mcp", "transport": "http", "tools": [ { "name": "build_context", "description": "One-call, token-bounded task packet ..." }, { "name": "digest", "description": "Ingest extracted triples ..." }, ... ] } Agent card (A2A) The A2A-compatible agent card describes the service as an agent with named skills corresponding to the MCP tools: curl https://abagraph.com/.well-known/agent-card.json API catalog (RFC 9728) The /.well-known/api-catalog file follows RFC 9728 and links the OpenAPI spec and the full docs: curl https://abagraph.com/.well-known/api-catalog { "linkset": [{ "anchor": "https://abagraph.com", "service-desc": [{ "href": "https://abagraph.com/openapi.json", "type": "application/openapi+json" }], "service-doc": [{ "href": "https://abagraph.com/llms-full.txt", "type": "text/markdown" }] }] } index.md A Markdown homepage for agents and clients that prefer text/markdown. Also served when the Accept: text/markdown header is present on a GET / request: curl https://abagraph.com/index.md # or curl -H "Accept: text/markdown" https://abagraph.com/ HTTP Link header Every public response from the server carries a Link HTTP header that points to all key discovery documents. This allows agents and crawlers to discover the API from any URL they visit: curl -I https://abagraph.com/llms.txt | grep -i link Link: </sitemap.xml>; rel="sitemap", </index.md>; rel="alternate"; type="text/markdown", </openapi.json>; rel="service-desc", </.well-known/api-catalog>; rel="api-catalog", </llms.txt>; rel="describedby" GET /api/connect For authenticated agents the /api/connect endpoint returns the tenant's connection details and copy-paste snippets, including a pre-filled MCP host configuration. The real token is never echoed; the snippet uses the <YOUR_API_KEY> placeholder. curl https://acme.abagraph.com/api/connect \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" The response includes a ready-to-use snippets.mcp_config block: { "mcpServers": { "abagraph": { "url": "https://acme.abagraph.com/mcp", "headers": { "Authorization": "Bearer <YOUR_API_KEY>" }, "tools": ["build_context","digest","remember","recall","whats_changed"] } } } CLI init The Node CLI generates a local abagraph.mcp.json configuration file: npx abagraph agent init This calls GET /api/connect and writes the MCP config in a format ready for Claude Desktop or Cursor. MCP Server Setup — configure and connect the MCP server Tools Reference — all seven MCP tools Agent Integration Overview ### Per-Turn Memory Loop (agents) Per-Turn Memory Loop TL;DR Session start : call build_context (MCP) or loadContext (SDK) once. The returned packet is the frozen context for this session. Turn : inject the packet into the model. Use recall for targeted mid-turn lookups. Session end : persist new facts with remember / digest (MCP) or remember / digest (SDK). Why frozen snapshots Agent sessions benefit from a single, stable context snapshot rather than live-queried memory on every token. A frozen snapshot gives the model a deterministic view; facts written during the turn are persisted but are not reflected in the current context object. This avoids mid-session inconsistency and keeps token budgets predictable. The next session call to build_context will incorporate everything written during the previous session. Step 1: Session start Call build_context once with the task and optional seed entities. Receive a token-bounded packet of confidence-ranked facts plus adjacent wiki-page snippets. Keep this packet for the life of the session. Via MCP { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "build_context", "arguments": { "task": "plan the Q3 roadmap for Project Atlas", "namespace": "acme", "seeds": ["Project:atlas", "Person:alice"], "max_facts": 32, "max_tokens": 4096 } } } Via TypeScript SDK import { createAgentMemory } from "@abagraph/client/agent"; const mem = createAgentMemory({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY, namespace: "acme", agent: "roadmap-bot", defaultBudget: 4096, }); // loadContext returns a frozen ContextPacket const ctx = await mem.loadContext("plan the Q3 roadmap for Project Atlas", { seeds: ["Project:atlas", "Person:alice"], maxFacts: 32, }); Context packet shape { "summary": "Alice leads Project Atlas. Q2 status: on track.", "entities": ["Project:atlas", "Person:alice"], "facts": [ { "id": "01J...", "subject": "Project:atlas", "predicate": "lead", "object": "Person:alice", "valid_from": 1718841600000, "valid_until": null, "confidence": 0.95, "source": "meeting-notes", "status": "Active", "corroboration": { "agree": 2, "conflict": 0, "sources": ["meeting-notes", "slack"] } } ], "pages": [ { "id": "page:atlas-brief", "title": "Atlas Brief", "source": "wiki", "snippet": "Project Atlas targets SMB clients in the EU..." } ], "tokens": 280, "total_tokens": 4096, "collapsed": 0, "counts": { "facts": 1, "pages": 1, "entities": 2 } } Step 2: Turn Inject the context packet into the model's system prompt or user message. The facts array is already sorted by confidence and trimmed to the token budget. For mid-turn targeted lookups — when you know a specific entity or predicate — use recall in pattern or walk mode. Avoid semantic recall mid-turn unless the embedding provider latency is acceptable; defer bulk retrieval to the next session start. async function runTurn(ctx: ContextPacket, userMessage: string): Promise { const systemPrompt = formatContextAsPrompt(ctx); return await myModel.chat([ { role: "system", content: systemPrompt }, { role: "user", content: userMessage }, ]); } Step 3: Session end After the turn, persist any new facts the agent learned or produced. Use remember for a single fact, or digest for a batch of extracted triples. Via MCP: single fact { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "remember", "arguments": { "subject": "Project:atlas", "predicate": "status", "object": "Q3 planning in progress", "namespace": "acme", "source": "roadmap-bot", "confidence": 0.9 } } } Via MCP: bulk digest { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "digest", "arguments": { "namespace": "acme", "source": "roadmap-bot:session-2026-06-22", "facts": [ { "subject": "Project:atlas", "predicate": "status", "object": "Q3 planning", "confidence": 0.9 }, { "subject": "Sprint:47", "predicate": "owner", "object": "Person:alice", "confidence": 0.85 } ] } } } Via TypeScript SDK // Option A: turn() wraps the whole loop automatically const result = await mem.turn("plan Q3 roadmap", async (ctx) = { const output = await runTurn(ctx, userMessage); return { result: output, facts: [ { subject: "Project:atlas", predicate: "status", object: "Q3 planning", confidence: 0.9, reason: "user confirmed during session" } ], }; }); // Option B: explicit loadContext + remember const ctx = await mem.loadContext("plan Q3 roadmap"); const output = await runTurn(ctx, userMessage); await mem.remember([ { subject: "Project:atlas", predicate: "status", object: "Q3 planning", confidence: 0.9 } ]); The RememberInput type extends FactInput with optional reason , why , and how string fields. These are folded into the fact's properties object before writing, preserving agent reasoning alongside the fact. ADK-style runner plugin If your runner supports ADK-style session hooks, install the memoryPlugin once at the runner level. Individual agents need no extra wiring. import { memoryPlugin } from "@abagraph/client/plugin"; const plugin = memoryPlugin({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY, namespace: "acme", agent: "reels-bot", }); // At session start — call loadContext and inject the frozen packet const ctx = await plugin.onSessionStart({ task: "plan the Q3 roadmap", seeds: ["Project:atlas"], budget: 4096, }); // ... run agent with ctx ... // At session end — fire-and-forget; does not block the caller plugin.onSessionEnd({ content: transcript, // raw text to digest // or: facts: [...], // pre-extracted triples to remember }); Fire-and-forget write-back. onSessionEnd does not return a promise and does not await the write. If the write fails the error is swallowed. For critical knowledge use mem.digest() or mem.remember() and await the result explicitly before ending the session. Durable agent runs abagraph persists each turn as an atomic step in the run log, enabling durable, resumable agent sessions. A run can be paused and re-entered with the same run identifier; each step and its associated facts remain queryable via the REST API for audit and time-travel inspection. Periodic consolidation After several sessions, accumulated facts may contain near-duplicates. Call POST /api/consolidate (REST) or mem.consolidate() (SDK) periodically — for example at the end of a work-day batch or on a scheduled basis — to collapse duplicate facts and surface contradictions. Tools Reference — build_context, remember, digest parameter docs Tool Profiles — restrict the tool list per role MCP Server Setup — configure the server Agent Integration Overview ### Tool Profiles (agents) Tool Profiles TL;DR Set ABAGRAPH_MCP_PROFILE=agent-memory for write-capable agents: exposes build_context , digest , remember , recall , whats_changed (5 tools). Set ABAGRAPH_MCP_PROFILE=read-only for read-only agents: exposes recall , recall_as_of , explain_fact (3 tools). Unset exposes all seven tools. What profiles are When an LLM host calls tools/list , abagraph returns the JSON Schema for every available tool. With all seven tools visible, a model may choose between overlapping tools (e.g. recall_as_of vs recall with as_of ) and degrade its tool-selection accuracy. The ABAGRAPH_MCP_PROFILE environment variable filters tools/list down to a role-appropriate subset. Dispatch still works for any of the seven names regardless of the profile — the profile only affects what is advertised to the model. Available profiles agent-memory Optimized for the typical session pattern: load context, read, write, diff. Drops the specialized recall_as_of and explain_fact tools to keep the tool list tight. export ABAGRAPH_MCP_PROFILE=agent-memory Tools exposed: build_context , digest , remember , recall , whats_changed read-only Suitable for agents that consume memory but must not write it (auditors, readers, analyzers). The write tools are not advertised, so a model following its tool list cannot assert or digest facts. export ABAGRAPH_MCP_PROFILE=read-only Tools exposed: recall , recall_as_of , explain_fact Profiles are advisory, not a security boundary. Setting the profile only controls which tools appear in tools/list . The tool dispatcher will still execute any named tool. To prevent writes in production, use a read-only bearer token (tenant tokens have no built-in write restriction today) or remove write access at the network layer. Unset (all tools) When ABAGRAPH_MCP_PROFILE is not set or is empty, all seven tools are advertised. Use this for development and for privileged agents that need the full surface. All seven tools at a glance Tool Direction agent-memory read-only unset build_context read yes no yes digest write yes no yes remember write yes no yes recall read yes yes yes whats_changed read yes no yes recall_as_of read no yes yes explain_fact read no yes yes Configuration Set ABAGRAPH_MCP_PROFILE in your environment before the service starts. For a managed tenant, configure the variable in your tenant settings. For a dedicated deployment, set it in your container environment: export ABAGRAPH_MCP_PROFILE=agent-memory How it works The profile is read from the ABAGRAPH_MCP_PROFILE environment variable at each tools/list request. Only the schemas for the profile's allowed tools are returned in the response. The tools/call dispatch path does not consult the profile: it matches tool names unconditionally, so calling a tool by name always works regardless of the active profile. The read-only profile reduces discoverability and is NOT a security boundary — do not rely on it to prevent writes. Use token-level access controls or network restrictions to enforce write prevention. Tools Reference — full parameter docs for all seven tools MCP Server Setup — configure and verify the server Per-Turn Memory Loop — when to use which tools Agent Integration Overview ### MCP Tools Reference (agents) MCP Tools Reference TL;DR Seven tools: digest , remember , recall , recall_as_of , whats_changed , build_context , explain_fact . Every tool requires namespace . Timestamps are unix milliseconds (integer). Tool responses are JSON payloads wrapped in content[0].text ; never contain raw embeddings. Common conventions namespace — required on every call. Scopes the read/write to one tenant. Timestamps — all as_of , from , to , valid_from , valid_until values are unix milliseconds (integer). fact_view — the bounded projection returned in every facts array: id , subject , predicate , object , valid_from , valid_until , confidence , source , status . No embeddings. Limits — default limit 20, maximum 200. Walk depth maximum 6. Error response — { "content": [{ "type": "text", "text": "<message>" }], "isError": true } . digest Bulk-ingest a list of already-extracted subject-predicate-object triples into bitemporal memory with provenance. The calling model does the extraction; digest handles embedding, PII masking, conflict detection, and supersession. Use for documents and conversations. For a single fact use remember . Parameters Field Type Req? Description facts array yes Array of triple objects (see below) namespace string yes Tenant scope source string no Default provenance for triples that omit their own source Each item in facts : Field Type Req? Description subject string yes Entity ref, e.g. Person:alice predicate string yes Relation, e.g. works_at object any yes Entity ref string, or literal (string, number, bool) source string no Per-item provenance (overrides top-level source ) confidence number 0..1 no Below threshold: lands as Candidate , does not supersede coexist boolean no Allow multiple active objects for this subject+predicate (set-valued) valid_from integer no World-time start (unix ms); defaults to now valid_until integer no World-time end (unix ms); omit while fact is still true properties object no Free-form JSON attached to the fact (e.g. supporting quote) Response { "written": 3, "written_ids": ["01J...", "01J...", "01J..."], "superseded": 1, "candidates": 0, "entities": ["Person:alice", "Org:acme"], "pii_masked": false } Example { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "digest", "arguments": { "namespace": "acme", "source": "meeting-notes-2026-06", "facts": [ { "subject": "Person:alice", "predicate": "works_at", "object": "Org:acme", "confidence": 0.9 }, { "subject": "Person:alice", "predicate": "role", "object": "Engineer", "confidence": 0.85 }, { "subject": "Project:atlas", "predicate": "lead", "object": "Person:alice", "confidence": 0.95 } ] } } } remember Write one fact with provenance. Auto-supersedes a conflicting prior fact for the same subject+predicate (unless coexist=true ). Returns the written id, status, and any fact ids that were closed. Use for single in-the-loop writes; use digest for bulk. Parameters Field Type Req? Description subject string yes Entity ref, e.g. Person:alice predicate string yes Relation, e.g. works_at object any yes Entity ref string or literal namespace string yes Tenant scope source string no Provenance, e.g. user or tool:web confidence number 0..1 no Below threshold: lands as Candidate coexist boolean no Allow multiple active objects (set-valued predicates) Response { "written_id": "01J...", "status": "Active", "superseded": ["01J..."], "pii_masked": false } recall Retrieve active facts three ways via mode : semantic (embedding-based kNN), pattern (exact triple filter), or walk (BFS from an entity). Capped by limit (default 20, max 200). For historical recall use recall_as_of ; for a full task packet use build_context . Parameters Field Type Req? Description mode string yes semantic | pattern | walk namespace string yes Tenant scope query string semantic Natural-language question for kNN matching as_of integer no semantic: time-travel — match facts valid at this unix ms instant subject string no pattern: entity ref filter predicate string no pattern: relation filter object any no pattern: object filter start string walk Entity ref to walk from depth integer no walk: hops (default 1, max 6) direction string no walk: out | in | both (default out ) edge_types string[] no walk: restrict to these predicates min_confidence number no Drop facts below this confidence limit integer no Max facts returned (default 20, max 200) Response { "mode": "semantic", "count": 5, "facts": [ /* fact_view objects */ ] } Example: semantic recall { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "recall", "arguments": { "mode": "semantic", "namespace": "acme", "query": "Who leads project Atlas?", "limit": 10 } } } Example: graph walk { "name": "recall", "arguments": { "mode": "walk", "namespace": "acme", "start": "Person:alice", "depth": 2, "direction": "out", "edge_types": ["works_at", "leads"] } } recall_as_of Time-travel: return facts that were active at a past instant. Reveals facts that have since been superseded. Pair with whats_changed to measure how memory evolved between two times. Parameters Field Type Req? Description as_of integer yes Unix ms timestamp to view at namespace string yes Tenant scope subject string no Filter by entity predicate string no Filter by relation object any no Filter by object limit integer no Max facts (default 20, max 200) Response { "as_of": 1718928000000, "count": 4, "facts": [ /* fact_view objects */ ] } whats_changed Diff memory between two instants. Returns which facts were added after from , which were superseded or removed, and how many still hold. The clean way to answer "what is new since my last checkpoint". Parameters Field Type Req? Description from integer yes Earlier unix ms timestamp to integer yes Later unix ms timestamp namespace string yes Tenant scope subject string no Scope to one entity predicate string no Scope to one relation object any no Scope to one object value Response { "from": 1718841600000, "to": 1718928000000, "added": [ /* fact_view objects: present in 'to' but not 'from' */ ], "superseded": [ /* fact_view objects: present in 'from' but not 'to' */ ], "still_valid": 12 } build_context The flagship tool. One call assembles a ready-to-use task packet: facts matching the goal, a one-hop graph walk around seed entities, adjacent wiki-page snippets, and corroboration signals — all scoped to namespace and trimmed to the token budget. Prefer this at task start over several recall calls. Parameters Field Type Req? Description task string yes What you are about to do — drives fact selection namespace string yes Tenant scope seeds string[] no Entity refs to walk from (e.g. ["Person:alice","Project:atlas"] ) max_facts integer no Cap on facts in packet (default 32, max 200) as_of integer no Build packet as memory stood at this unix ms instant max_tokens integer no Token budget; facts are confidence-ranked then trimmed to fit (default 4096, max 100000) style string no Format for system_prompt : claude (XML), openai (markdown), or generic (default) role string no Agent role text embedded in the system_prompt core. Empty = generic assistant role. Response { "system_prompt": "You are an AI assistant. Alice leads Project Atlas at Acme…", "summary": "Alice leads Project Atlas at Acme. Atlas is in planning phase.", "entities": ["Person:alice", "Project:atlas", "Org:acme"], "facts": [ { "id": "01J...", "subject": "Project:atlas", "predicate": "lead", "object": "Person:alice", "valid_from": 1718841600000, "valid_until": null, "confidence": 0.95, "source": "meeting-notes", "status": "Active", "corroboration": { "agree": 2, "conflict": 0, "sources": 2 } } ], "pages": [ { "id": "page:atlas-brief", "title": "Atlas Brief", "subject": "Project:atlas", "source": "wiki", "snippet": "Project Atlas targets SMB clients..." } ], "collapsed": 0, "tokens": 312, "total_tokens": 4096, "counts": { "facts": 1, "pages": 1, "entities": 3 } } system_prompt is a ready-to-paste system message formatted for your model (controlled by style ). Each fact in the facts array carries an inline corroboration field: agree and conflict are counts of other facts with the same or different object; sources is an integer count of distinct provenance strings that agree on this fact. explain_fact Drill into one fact: its source, confidence, status, validity window, and the full lifecycle (ordered by valid_from ) of every fact sharing the same subject+predicate — including superseded and retracted ones. Use to verify a recalled claim before relying on it. Parameters Field Type Req? Description fact_id string yes Id of the fact to explain namespace string yes Tenant scope; the fact must belong to this namespace Response { "fact": { "id": "01J...", "subject": "Person:alice", "predicate": "works_at", "object": "Org:acme", "valid_from": 1718841600000, "valid_until": 1718928000000, "recorded_at": 1718841605000, "confidence": 0.9, "source": "onboarding-form", "status": "Superseded", "properties": null }, "history": [ { /* fact_view of oldest entry for Person:alice works_at */ }, { /* fact_view of newer entry that superseded it */ } ], "note": "history = the subject+predicate lifecycle ordered by valid_from; supersession links are derived from validity intervals, not stored." } The fact object in explain_fact includes recorded_at and properties in addition to the standard fact_view fields. The history array uses the standard fact_view projection. MCP Server Setup — install and configure the server Tool Profiles — select a subset of tools with ABAGRAPH_MCP_PROFILE Per-Turn Memory Loop — how to sequence these tools Agent Integration Overview ### API Reference: /api/ask (api) API Reference: /api/ask TL;DR POST /api/ask — semantic search + BYOK LLM call = grounded answer. Evidence is gathered from the engine; the caller supplies their own model key. The API key in model is never persisted or logged — only a redacted form appears in telemetry. Returns answer , evidence facts, model_confidence , and token counts. Endpoint Method Path Auth POST /api/ask Bearer / cookie (tenant-scoped) Request body Field Type Required Description question string yes Natural-language question. model object (BYOK config) yes LLM to use. Fields: key (required), model (required), provider , endpoint . top_k integer no Number of evidence facts to retrieve via kNN. Default: 8. BYOK config object { "provider": "openai", "key": "sk-...", "model": "gpt-4o-mini", "endpoint": "https://api.openai.com/v1" } key and model are required. provider (default anthropic ) selects the wire format and default endpoint; endpoint overrides the base URL. There is no api_key or base_url field — use key and endpoint . See BYOK Model Keys for the full lifecycle. Response { "answer": "Alice works at Company:beta as Staff Engineer, effective 2024-06-01.", "evidence": [ {"id": "01J...", "subject": "Person:alice", "predicate": "works_at", "object": "Company:beta", "confidence": 0.95} ], "model_confidence": 0.92, "system_reliability": 0.91, "tokens": {"in": 312, "out": 48} } Field Description answer Plain-text answer produced by the model, grounded in evidence . evidence The kNN-retrieved facts used to ground the answer. Tenant-scoped. model_confidence Self-reported confidence extracted from the model response (0–1), or null if the model did not provide one. system_reliability Mean confidence of the evidence facts — a measure of how well-sourced the retrieval set is. tokens.in Prompt tokens consumed by the model call. tokens.out Completion tokens produced by the model call. Example curl -X POST "$ABAGRAPH_BASE_URL/api/ask" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "question": "Where does Alice work and what is her title?", "model": { "provider": "openai", "key": "sk-...", "model": "gpt-4o-mini", "endpoint": "https://api.openai.com/v1" }, "top_k": 10 }' Notes Requires an embedding provider ( ABAGRAPH_EMBEDDING ) to have been configured when facts were asserted. Evidence retrieval is kNN-based and returns an empty set if no embeddings exist. The API key in model travels only on the outbound request to the model provider. It is never written to storage and appears in logs only as a redacted stub. For richer context (graph walk + wiki pages), use POST /api/context to build the evidence set yourself and call your model client-side. API: /api/search — kNN retrieval without the LLM call API: /api/context — full structured context packet API: /api/digest — ingest documents to build the evidence base ### API Reference: /api/branches (api) API Reference: /api/branches TL;DR Branches are instant forks: a named overlay of facts on top of the parent snapshot at a chosen timestamp. Write to a branch via POST /api/facts?branch=NAME . Read a reconciled view (base + overlay) via GET /api/facts?branch=NAME . Promote a branch to merge its overlay back to the parent; discard to drop it. POST /api/branches Create a new branch. Branch names must be ASCII alphanumeric, dash, or underscore, at most 64 characters, and must not be main . Request body Field Type Required Description name string yes Branch name. ASCII alnum/dash/underscore, ≤64 chars. parent string no Parent branch or main . Default: main . as_of integer (Unix ms) no Fork point timestamp. Default: now. Response (201) { "name": "feature-alice-move", "owner": "acme", "parent": "main", "base_ts": 1717200000000, "status": "open", "created_at": 1717200001234 } Example curl -X POST "$ABAGRAPH_BASE_URL/api/branches" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"feature-alice-move","parent":"main"}' GET /api/branches List all branches owned by the caller's tenant. curl "$ABAGRAPH_BASE_URL/api/branches" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" GET /api/branches/:name Get metadata for a single branch. curl "$ABAGRAPH_BASE_URL/api/branches/feature-alice-move" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Branch-scoped fact reads and writes Use the ?branch=NAME query parameter on /api/facts to read or write on a branch. Read (reconciled view) # Returns parent snapshot merged with branch overlay curl "$ABAGRAPH_BASE_URL/api/facts?branch=feature-alice-move&subject=Person:alice" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Write (assert on the branch) curl -X POST "$ABAGRAPH_BASE_URL/api/facts?branch=feature-alice-move" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:alice","predicate":"works_at","object":"Company:gamma","source":"hr-draft"}' Branch writes do not touch the parent. The overlay fact is tagged with the branch name in its properties . GET /api/branches/:name/diff Show what changed on the branch relative to the parent snapshot. { "branch": "feature-alice-move", "parent": "main", "base_ts": 1717200000000, "added": [...], "changed": [...], "removed": [...], "counts": {"added": 1, "changed": 0, "removed": 0} } curl "$ABAGRAPH_BASE_URL/api/branches/feature-alice-move/diff" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" POST /api/branches/:name/promote Merge the branch overlay into the parent. Each active fact on the overlay is asserted in the parent's main tenant namespace using the standard supersede semantics. The branch is then marked promoted . curl -X POST "$ABAGRAPH_BASE_URL/api/branches/feature-alice-move/promote" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Response: {"ok":true,"branch":"feature-alice-move","promoted":1} POST /api/branches/:name/discard Drop all overlay facts and mark the branch discarded . Equivalent to DELETE /api/branches/:name . curl -X POST "$ABAGRAPH_BASE_URL/api/branches/feature-alice-move/discard" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Response: {"ok":true,"branch":"feature-alice-move","discarded":1} TypeScript SDK import { createClient } from '@abagraph/client' const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY }) // Create await db.branches.create({ name: 'feature-alice-move' }) // Assert on branch await db.assert({ subject: 'Person:alice', predicate: 'works_at', object: 'Company:gamma' }, { branch: 'feature-alice-move' }) // Diff const diff = await db.branches.diff('feature-alice-move') // Promote await db.branches.promote('feature-alice-move') // Discard await db.branches.discard('feature-alice-move') Errors Code HTTP Meaning SchemaInvalid 400 Invalid branch name (non-ASCII, too long, reserved). Conflict 409 Branch name already exists; or branch is not open for promote/write. NotFound 404 Branch does not exist. Notes Branch facts are stored under a derived tenant ( <owner>~<name> ) and tagged with the branch name in properties . The tenant_scope read chokepoint excludes branch-tagged facts from normal reads, so they are invisible outside their branch reconciliation path. Promote is not transactional across concurrent promotions of the same predicate — standard supersede semantics apply. If two branches both modified the same subject+predicate, the last promote wins. API: /api/facts — base fact read/write API: /api/tenants — tenant lifecycle (admin) API: Overview — auth and tenant scoping ### API Reference: Compact, Enrich, Consolidate (api) API Reference: Compact, Enrich, and Consolidate TL;DR POST /api/compact — stateless token budget trimmer: rank a fact list by confidence/recency and drop the tail to fit a budget. POST /api/enrich — pre-write inspector: PII-mask incoming facts and flag conflicts against known active facts. No write. POST /api/consolidate — write: collapse exact-duplicate active facts and surface remaining contradictions. POST /api/compact Trim a caller-supplied list of facts to fit a token budget using the same confidence/recency scoring that /api/context uses. Stateless : no read from or write to the engine. Request body Field Type Required Description facts array of Fact yes Facts to trim. Must be full Fact objects (with id , confidence , etc.). token_budget integer yes Target token count. Facts are ranked and dropped until the budget is met. Response { "facts": [...], "tokens": 387, "total_tokens": 1204, "dropped": 9 } Field Description facts Trimmed fact list, highest-scored first. tokens Estimated tokens in the returned facts. total_tokens Estimated tokens in the input set before trimming. dropped Number of facts removed to meet the budget. Example curl -X POST "$ABAGRAPH_BASE_URL/api/compact" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"facts":[...],"token_budget":512}' TypeScript SDK import { createClient } from '@abagraph/client' const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY }) const trimmed = await db.compact({ facts: myFacts, token_budget: 512 }) POST /api/enrich PII-mask a batch of incoming facts and check each one against the known active facts for the same subjects. No write : returns the scrubbed facts plus a conflicts list. Request body Field Type Required Description facts array of FactInput yes Incoming facts to inspect. source string no Default provenance label for facts that omit source . Response { "facts": [ {"subject":"Person:sarah","predicate":"works_at","object":"Company:nova","confidence":0.9,"source":"import"} ], "conflicts": [ { "subject": "Person:sarah", "predicate": "works_at", "incoming": "Company:nova", "existing": "Company:acme", "existing_id": "01J..." } ], "masked": 0 } conflicts lists cases where the incoming object differs from a current active fact for the same subject+predicate. Example curl -X POST "$ABAGRAPH_BASE_URL/api/enrich" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "facts": [ {"subject":"Person:sarah","predicate":"works_at","object":"Company:nova"} ], "source": "crm-import" }' TypeScript SDK const enriched = await db.enrich({ facts: incoming, mask: true }) POST /api/consolidate Scan all active facts for the tenant, collapse exact duplicates (same subject+predicate+object, multiple active copies) by retracting lower-scored duplicates in one atomic transact , then return the count of remaining contradictions. Write operation. Consolidate retract calls are permanent (hard retract, not supersede). Run GET /api/factcheck first to preview what it will collapse. Response {"collapsed": 4, "superseded": ["01J...","01J...","01J...","01J..."], "conflicts": 2} Field Description collapsed Number of duplicate facts retracted. superseded Ids of the retracted facts. conflicts Count of remaining contradictions (different objects for the same subject+predicate) that consolidate did NOT resolve. Use GET /api/factcheck to inspect them. Example curl -X POST "$ABAGRAPH_BASE_URL/api/consolidate" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" TypeScript SDK const report = await db.consolidate() Notes Consolidate does not resolve contradictions (different objects) — it only collapses exact duplicates. Contradictions must be resolved manually via retract or re-assert. The coexist predicates ( references , tag , knows , member_of , cites ) are skipped during consolidate — multiple active objects are expected for them. All three endpoints are tenant-scoped. Admin callers' results span all tenants. API: /api/factcheck — audit contradictions without writing API: /api/context — uses the same confidence/recency scoring as compact API: /api/digest — ingest facts with automatic PII masking ### API Reference: /api/connectors (api) API Reference: /api/connectors TL;DR A connector is durable config ( Connector:<name> fact) that ingests REAL data from an external source. webhook mode: POST events to /api/connectors/:name/events . poll mode: POST to /api/connectors/:name/pull and abagraph fetches the URL. All ingested facts flow through the shared PII-mask, supersede, and embed pipeline; asserted facts are published to the SSE feed. POST /api/connectors Register a new connector. Names are normalized to lowercase ASCII alphanumeric with underscores. Request body Field Type Required Description name string yes Unique connector name. Normalized to a slug. kind string no webhook (default) or poll . Poll connectors must have a url . label string no Human-readable display name. Defaults to name . url string poll only URL to fetch when /pull is triggered. SSRF-guarded. mapping object no Field-to-fact mapping config (see below). Mapping object Controls how each record from the source maps to subject/predicate/object triples. Each record is parsed to a flat object; the subject comes from a template , and every selected scalar field becomes one fact whose predicate is the field name and whose object is the field value. Field Type Required Description subject string (template) no Subject template. $field tokens are filled from the record — e.g. Contact:$id draws id from each record. With no $ , every fact shares this one fixed subject. A record whose tokens don't all resolve (a literal $ remains) is skipped — never a partial fact. Default: Record:$id . fields string array no Allow-list of field names to map. Each listed field becomes one fact: predicate = the field name, object = the field value. Omit or leave empty to map every scalar field. Fields consumed by the subject template are never re-emitted as predicates. confidence float 0–1 no Default per-fact confidence when a record carries none. Default: 0.9 . { "subject": "Contact:$id", "fields": ["full_name", "email", "status"], "confidence": 0.9 } A field value may be a bare scalar, or an object {"value": "...", "confidence": 0.7, "valid_from": 2026} so the source can carry per-fact trust and a bitemporal validity start (drift). Values below the confidence threshold land as Candidate (counted as held ) and do not supersede. Response (200) { "name": "crm_contacts", "kind": "webhook", "label": "CRM Contacts", "url": null } Example curl -X POST "$ABAGRAPH_BASE_URL/api/connectors" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "crm_contacts", "kind": "webhook", "label": "CRM Contacts", "mapping": { "subject": "Contact:$id", "fields": ["full_name", "email"] } }' GET /api/connectors List all connectors registered for the caller's tenant, with live ingested fact counts. curl "$ABAGRAPH_BASE_URL/api/connectors" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Response: {"connectors":[{"name","kind","label","url","subject","ingested"}]} GET /api/connectors/:name Get connector config and the 20 most recently ingested facts. curl "$ABAGRAPH_BASE_URL/api/connectors/crm_contacts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" POST /api/connectors/:name/events Accept an inbound webhook delivery. The body may be a single JSON object, a JSON array, or JSONL (one object per line). Each record is mapped to facts, PII-masked, and asserted. curl -X POST "$ABAGRAPH_BASE_URL/api/connectors/crm_contacts/events" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"id":"sarah","full_name":"Sarah Kim","email":"sarah@example.com"}' With the mapping above, id fills the Contact:$id subject template (yielding Contact:sarah ) and the two remaining fields are asserted as facts — so asserted is 2, not 3. Response {"connector": "crm_contacts", "asserted": 2, "held": 0, "superseded": 0, "facts": 2} Field Description asserted Facts written as Active. held Facts below confidence threshold — stored as Candidate, did not supersede. superseded Previously active facts closed by this ingestion. facts Total facts touched (asserted + held). POST /api/connectors/:name/pull Trigger an outbound fetch of the connector's configured URL, then digest the response. The HTTP fetch is non-blocking — a slow source does not delay other writes. The managed service handles durability and consistency throughout the pull. curl -X POST "$ABAGRAPH_BASE_URL/api/connectors/crm_contacts/pull" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" SSRF guard. The managed service blocks outbound fetches to private, loopback, and cloud-metadata addresses. DELETE /api/connectors/:name Delete the connector configuration. Ingested facts are NOT deleted — they remain in the fact store with source=<name> . curl -X DELETE "$ABAGRAPH_BASE_URL/api/connectors/crm_contacts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Response: {"deleted":"crm_contacts"} Background polling Set ABAGRAPH_CONNECTOR_POLL_SECS=60 to enable automatic background polling of all registered poll connectors at that interval. The default (0) disables automatic polling — use /pull explicitly instead. API: /api/digest — manual one-shot ingestion API: Stream (SSE) — observe ingested facts in real time API: /api/factcheck — surface contradictions after ingestion ### API Reference: /api/context (api) API Reference: /api/context TL;DR GET or POST /api/context — the flagship agent endpoint. One call returns a ranked, deduplicated, corroboration-annotated bundle of facts and wiki pages for an LLM turn. Set token_budget and facts are trimmed by confidence/recency to fit. Without it, up to max_facts (default 64) are returned. Results include collapsed (duplicates removed) and corroboration (per-fact agree/conflict count). Endpoint Method Path Auth GET /api/context Bearer / cookie (tenant-scoped) POST /api/context Bearer / cookie (tenant-scoped) GET and POST are equivalent. Use POST with a JSON body for production calls; GET with query parameters is useful for quick browser or curl exploration. Request (POST body or GET query params) Field Type Default Description goal string "" The task or question the agent is working on. Used for semantic ranking and the packet summary. seeds array of EntityRef [] Starting entities (e.g. "Person:sarah" ). Their facts plus 1-hop neighbors are included first. max_facts integer 64 Maximum number of facts returned when no token_budget is set. max_pages integer 8 Maximum wiki page snippets to include. token_budget integer null When set, facts are ranked by confidence and recency then trimmed to fit the budget. Overrides max_facts . as_of integer (Unix ms) null (now) Time-travel snapshot. Return facts valid at this timestamp. project EntityRef null Focus the context on a project entity. cluster_threshold float 0–1 0.0 (off) Cosine threshold for near-duplicate clustering after dedup. Typical value: 0.97. compact_model object (BYOK config) null When set, the budget-dropped tail is folded into one derived digest fact via this model. Omit for offline/deterministic path. include_recent boolean true Include recently asserted facts even if they did not match seeds. include_conflicts boolean true Include facts with contradictions (surfaced in corroboration). style string "generic" Format for the returned system_prompt : claude (XML tags), openai (markdown), or generic . role string "" Agent role or mission text embedded in the system_prompt core section. Omit for a generic system prompt. Response { "kind": "context_packet", "summary": "Alice works at Company:beta as of 2024-06-01. Known skills: Rust, systems.", "system_prompt": "You are an AI assistant with access to verified memory about Alice…", "entities": ["Person:alice", "Company:beta"], "facts": [ { "id": "01J...", "subject": "Person:alice", "predicate": "works_at", "object": "Company:beta", "confidence": 0.95, "status": "active", "valid_from": 1717200000000 } ], "pages": [ {"id": "Page:alice", "subject": "Person:alice", "title": "Alice", "body": "…", "source": null} ], "tokens": 412, "total_tokens": 1204, "collapsed": 3, "corroboration": [ {"agree": 2, "conflict": 0, "sources": 2} ], "warnings": [], "explain": {"steps": ["seeds→facts", "1-hop walk", "wiki lookup", "rank+trim"]} } Response fields Field Description summary One-paragraph plain-text summary of the returned facts. system_prompt Ready-to-paste system prompt derived from the summary and facts. Inject directly into the LLM system message. Format controlled by the style request field ( claude , openai , or generic ). entities Distinct entity refs referenced by the returned facts. facts Ranked, deduplicated, tenant-scoped facts. Full Fact objects without embedding vectors. pages Wiki page snippets for entities that have them. tokens Estimated tokens in the returned facts (after budget trim). Always present; computed even when no token_budget is set. total_tokens Estimated tokens in the full candidate set before trimming. Always present. total_tokens - tokens = reduction achieved by budget. collapsed Number of duplicate or near-duplicate facts removed by dedup/clustering. corroboration Index-aligned with facts . Each entry: {agree, conflict, sources} where sources is an integer count of distinct provenance strings that agree on this fact (not an array of names). derived_facts Facts synthesized from the dropped tail when compact_model is set. Omitted from the response on the default offline path. warnings Non-fatal advisory messages (e.g. seed entity not found). Example curl -X POST "$ABAGRAPH_BASE_URL/api/context" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "goal": "Draft a status update for Alice", "seeds": ["Person:alice"], "token_budget": 1024, "max_pages": 4 }' TypeScript SDK import { createClient } from '@abagraph/client' const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY }) const packet = await db.context({ goal: 'Draft a status update for Alice', seeds: ['Person:alice'], token_budget: 1024, }) // Inject packet.system_prompt into the LLM system message; use packet.facts for citations Agent memory SDK import { createAgentMemory } from '@abagraph/client/agent' const mem = createAgentMemory({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY, namespace: 'acme', agent: 'status-bot', }) // loadContext wraps /api/context — frozen snapshot for the session const ctx = await mem.loadContext('Draft a status update for Alice', { seeds: ['Person:alice'], budget: 1024, }) Notes The context packet is deterministic when no compact_model is set — the same request at the same time returns the same result. Branch reads: pass ?branch=NAME via the query string (GET mode) or set it via the project field combined with a branch-scoped token. Branch overlay facts are reconciled with the base before ranking. Admin callers may set tenant in the body to target a specific tenant. API: /api/search — semantic kNN without the full packet API: Compact / Enrich / Consolidate — client-side budget trimming API: Graph and Walk — graph traversal used internally by context ### API Reference: /api/digest (api) API Reference: /api/digest TL;DR POST /api/digest — two modes: (a) raw text/content: load → chunk → BYOK extract → judge → assert; (b) facts array: bulk pre-extracted triples → PII mask → supersede → embed → assert. PII is scrubbed before storage or embedding. Keys in extract_model / judge_model are never persisted or logged. Returns a receipt with counts: written , superseded , candidates , pii_masked . Endpoint Method Path Auth POST /api/digest Bearer / cookie (tenant-scoped) Mode A: raw content Load a document, chunk it, extract facts with a BYOK model, judge each candidate, and assert those that pass the confidence threshold. Request body Field Type Required Description content string yes Raw text or document content to ingest. content_type string no MIME type. Default: text/markdown . source string no Provenance label stamped on every asserted fact. Default: upload . extract_model object (BYOK config) no Model used to extract candidate facts from chunks. Falls back to model if absent. judge_model object (BYOK config) no Model used to score each candidate. Falls back to model if absent. model object (BYOK config) no Shared fallback for both extract and judge. Required if neither extract_model nor judge_model is given. threshold float 0–1 no Minimum judge score to assert a fact. Default: engine default. mask string no PII masking strategy: redact (default) | hash | shift_date . BYOK config object { "provider": "openai", "key": "sk-...", "model": "gpt-4o-mini", "endpoint": "https://api.openai.com/v1" } Field Required Description key yes Your model API key. Used only for the outbound model call; never persisted or logged (telemetry shows a redacted form). model yes Model id, e.g. gpt-4o-mini or claude-opus-4-5 . provider no anthropic (default) or openai -compatible. Selects the request wire format and the default endpoint. endpoint no Override the base URL (for Azure OpenAI, a local server, etc.). Defaults to the provider's standard endpoint. Note: the recognized fields are key and endpoint — there is no api_key or base_url field. A missing key or model returns SchemaInvalid (400) before any outbound call. The provider is taken from the explicit provider field (it is not inferred from the URL); the default is anthropic . Example curl -X POST "$ABAGRAPH_BASE_URL/api/digest" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "content": "Alice joined Company:beta on 2024-06-01 as Staff Engineer.", "content_type": "text/plain", "source": "hr-doc-2024", "model": {"provider":"openai","key":"sk-...","model":"gpt-4o-mini","endpoint":"https://api.openai.com/v1"}, "threshold": 0.8, "mask": "redact" }' Mode B: pre-extracted facts array Send a batch of FactInput objects directly. Skips extraction and judging; applies PII masking, supersede semantics, and embedding to each fact. Request body { "facts": [ {"subject": "Person:alice", "predicate": "works_at", "object": "Company:beta", "confidence": 0.95}, {"subject": "Person:alice", "predicate": "title", "object": "Staff Engineer", "confidence": 0.9} ], "source": "hr-system" } source at the top level is a default; individual facts may override it with their own source field. Example curl -X POST "$ABAGRAPH_BASE_URL/api/digest" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "facts": [ {"subject":"Person:alice","predicate":"works_at","object":"Company:beta"}, {"subject":"Person:alice","predicate":"title","object":"Staff Engineer"} ], "source": "hr-import" }' Response { "written": 2, "written_ids": ["01J...", "01J..."], "superseded": 1, "candidates": 0, "entities": ["Person:alice", "Company:beta"], "pii_masked": 0 } Field Description written Number of facts successfully asserted as Active. written_ids Fact ids of the newly asserted facts. superseded Number of previously active facts that were automatically closed. candidates Facts below the confidence threshold: stored as Candidate, did not supersede. entities Distinct entity refs referenced by the written facts. pii_masked Number of values redacted before storage. TypeScript SDK import { createClient } from '@abagraph/client' import { createAgentMemory } from '@abagraph/client/agent' const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY }) // Mode A — raw content (fire-and-forget in agent context) await db.digest({ content: 'Alice joined Company:beta as Staff Engineer.', content_type: 'text/plain', source: 'hr-doc', }) // Mode B via agent memory const mem = createAgentMemory({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY, namespace: 'acme', agent: 'bot' }) await mem.remember([ { subject: 'Person:alice', predicate: 'works_at', object: 'Company:beta', source: 'session' } ]) Notes PII detection runs before embedding. Detected values (emails, phone numbers, SSN patterns) are scrubbed according to the mask strategy before any write or vector computation. The max concurrent upload slots is controlled by ABAGRAPH_MAX_CONCURRENT_UPLOADS (default 4). Excess requests are queued. All asserted facts are published to the SSE feed ( GET /api/stream ). API: /api/facts — single-fact assert API: /api/transact — atomic multi-fact CAS write API: Compact / Enrich / Consolidate — pre-write enrichment and post-write dedup API: /api/connectors — continuous source integration ### API Reference: Entities and Predicates (api) API Reference: Entities and Predicates TL;DR GET /api/entities — list all entities with their fact counts. GET /api/entities/:id — resolve a single entity and all its active facts. GET /api/predicates — list the predicate vocabulary for the tenant. All endpoints are read-only and tenant-scoped. GET /api/entities Returns a list of all known entity refs (subjects) with their fact count. Tenant-scoped: non-admin callers only see entities that have at least one fact belonging to their tenant. Response [ {"id": "Person:alice", "kind": "Person", "name": "alice", "fact_count": 7}, {"id": "Company:beta", "kind": "Company", "name": "beta", "fact_count": 3}, {"id": "orphan-node", "kind": null, "name": "orphan-node", "fact_count": 1} ] kind is the part before the first : in the entity ref; name is the part after. For entities without a colon (bare ids), kind is null. Example curl "$ABAGRAPH_BASE_URL/api/entities" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" GET /api/entities/:id Resolve a single entity by its full ref (URL-encoded). Returns the entity metadata plus all its active facts. Path parameter Param Description :id Entity ref, URL-encoded. Example: Person%3Aalice for Person:alice . Response (200) { "id": "Person:alice", "kind": "Person", "facts": [ {"id": "01J...", "subject": "Person:alice", "predicate": "works_at", "object": "Company:beta", "status": "active"}, {"id": "01J...", "subject": "Person:alice", "predicate": "title", "object": "Staff Engineer", "status": "active"} ] } Response (404) {"error": {"code": "NotFound", "message": "no entity: Person:unknown"}} Example curl "$ABAGRAPH_BASE_URL/api/entities/Person%3Aalice" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" GET /api/predicates Returns the sorted list of all predicate names seen in the tenant's fact store. Admin callers get the global predicate index; non-admin callers see only predicates used in their own facts. Response ["cites", "knows", "member_of", "references", "tag", "title", "works_at"] Example curl "$ABAGRAPH_BASE_URL/api/predicates" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Notes Entity listing is derived by scanning all subjects in the storage index. For large stores, this may take longer than a targeted GET /api/facts?subject= query. Both endpoints support ?explain=1 for query plan inspection. Set-valued predicates (those asserted with policy: "coexist" ) appear once in the predicate list regardless of how many values they carry. API: /api/facts — filter facts by subject, predicate, or object API: Graph and Walk — graph view of entity relationships API: /api/factcheck — surface contradictions across entities ### API Reference: /api/factcheck (api) API Reference: /api/factcheck TL;DR GET /api/factcheck — scan for contradictions: multiple active values for a functional predicate, expired facts still marked active, and candidates that contradict an active fact. Read-only and tenant-scoped. Use it after ingestion to audit consistency. Set-valued predicates ( references , tag , knows , member_of , cites ) are never flagged as contradictions. Endpoint Method Path Auth GET /api/factcheck Bearer / cookie (tenant-scoped) Query parameters Param Type Description subject string Limit check to this entity ref. predicate string Limit check to this predicate. coexist CSV string Additional predicates to treat as set-valued (multiple active values allowed). Appended to the built-in list. Response { "contradictions": [ { "subject": "Person:alice", "predicate": "works_at", "kind": "multiple_active", "facts": [ {"id": "01J...", "object": "Company:acme", "confidence": 0.95, "status": "active", "source": "hr-system", "valid_until": null}, {"id": "01J...", "object": "Company:beta", "confidence": 0.90, "status": "active", "source": "linkedin", "valid_until": null} ] } ], "count": 1 } Contradiction kinds Kind Meaning multiple_active Two or more active facts share the same subject+predicate but have different objects. Only flagged for functional predicates. expired_but_active A fact has valid_until in the past but its status is still active . The TTL sweeper or a manual retract should close it. candidate_conflict A Candidate fact (below the confidence threshold) conflicts with an existing active fact for the same subject+predicate. Example # Check all facts for the tenant curl "$ABAGRAPH_BASE_URL/api/factcheck" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Narrow to a specific entity and predicate curl "$ABAGRAPH_BASE_URL/api/factcheck?subject=Person:alice&predicate=works_at" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Mark 'department' as a custom coexist predicate curl "$ABAGRAPH_BASE_URL/api/factcheck?coexist=department,role" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" TypeScript SDK import { createClient } from '@abagraph/client' const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY }) const report = await db.factcheck({ subject: 'Person:alice' }) console.log(report.contradictions) Built-in set-valued predicates The following predicates are always treated as set-valued and are never flagged as multiple_active : references tag knows member_of cites Add your own with the coexist query parameter. Notes Factcheck is the read counterpart to POST /api/consolidate , which resolves exact duplicates as a write. Factcheck surfaces contradictions (different objects) but does not resolve them — that requires explicit retract or re-assert. The scan covers Active and Candidate facts only. Superseded and Retracted facts are not checked. API: Compact / Enrich / Consolidate — post-write dedup and conflict surface API: /api/facts — retract a specific fact by id API: /api/connectors — use factcheck after connector ingestion ### API Reference: /api/facts (api) API Reference: /api/facts TL;DR GET /api/facts — list/filter facts. POST /api/facts — assert one fact. DELETE /api/facts/:id — retract (close validity; not a hard delete). Append ?explain=1 to any request for a query-plan envelope. All errors: {"error":{"code":"…","message":"…"}} . GET /api/facts Returns facts matching the given filters. Auth required (Bearer token or session cookie). Query parameters Param Type Description subject string EntityRef, e.g. Person:alice . Exact match. predicate string Predicate name, e.g. works_at . object string Object value or EntityRef, e.g. Company:acme . status string active (default), superseded , candidate , retracted . as_of integer (Unix ms) Point-in-time read: return facts valid at this Unix millisecond timestamp. limit integer Max results; default 200. Applied after tenant scoping so the page always contains up to N matching facts. tx_gt integer Only facts with tx > this value (streaming poll pattern). explain 1 Wrap response in {result, explain:{op,plan,stats,steps,total_ms}} . Example curl -H "Authorization: Bearer $TOKEN" \ 'https://acme.abagraph.com/api/facts?subject=Person:alice&limit=10' Response [ { "id": "01JXXXXXXXXXXXXXXXXXXXXXXX", "subject": "Person:alice", "predicate": "works_at", "object": "Company:acme", "valid_from": 1718000000000, "valid_until": null, "recorded_at": 1718000001200, "confidence": 0.95, "source": "onboarding-form", "status": "active", "tenant": "demo", "tx": 42 } ] POST /api/facts Assert a single fact. Returns the newly created Fact . If the predicate is functional (default), an existing active fact for the same subject+predicate is automatically superseded. Request body (FactInput) Field Type Required Description subject string yes EntityRef: Type:id predicate string yes Predicate name object string | number | boolean | null no Fact value; optional, defaults to null. Walk traversal only follows facts whose object resolves to an entity ref. valid_from integer (Unix milliseconds) no Defaults to now valid_until integer (Unix milliseconds) | null no null = open-ended confidence float 0–1 no Default 1.0; below threshold → Candidate source string no Provenance label properties object no Free-form JSON annotations policy string no auto_supersede (default) | coexist | reject . Case-insensitive. transient boolean no If true, the TTL sweeper hard-deletes this fact when valid_until passes. Pair with valid_until . Example curl -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:alice","predicate":"works_at","object":"Company:acme","confidence":0.95,"source":"hr-system"}' \ 'https://acme.abagraph.com/api/facts' DELETE /api/facts/:id Retract a fact by id: closes its validity interval (sets valid_until to now). The fact remains in history with status retracted . Does not hard-delete. curl -X DELETE -H "Authorization: Bearer $TOKEN" \ 'https://acme.abagraph.com/api/facts/01JXXXXXXXXXXXXXXXXXXXXXXX' Response: {"ok":true,"id":"01J..."} Errors Code HTTP Meaning NotFound 404 Fact id does not exist. Conflict 409 Reject policy and a conflicting fact is already active. SchemaInvalid 400 Malformed subject/predicate/object or missing required field. Unauthorized 401 Missing or invalid bearer token / session. Mental Model — why retract ≠ delete Guide: Assert a fact — Rust-side equivalent POST /api/transact — atomic multi-fact CAS batches GET /api/context — one-call context packet for agents ### API Reference: /api/graph and /api/walk (api) API Reference: /api/graph and /api/walk TL;DR GET /api/graph — returns a bucketed node+edge view of all tenant facts. Optional ?q= focuses on matching entities and their 1-hop neighborhood. GET /api/walk — BFS traversal from a named start entity. Control depth, direction, edge types, and time-travel point. Both endpoints are tenant-scoped and support ?explain=1 . GET /api/graph Query parameters Param Type Default Description q string — Focus filter: match entity name, predicate, or object. Matching entities and their 1-hop neighbors are included. Caps at 80 nodes. limit integer 0 (no limit) Return only the top-N nodes by degree. When q is set, capped at 80. Response { "nodes": [ {"id": "Person:alice", "name": "alice", "kind": "Person", "degree": 4, "fact_count": 6} ], "edges": [ {"id": "01J...", "from": "Person:alice", "to": "Company:beta", "predicate": "works_at", "confidence": 0.9} ], "total_nodes": 42, "total_edges": 87, "limited": false } Node fields: id (entity ref), name (part after the : ), kind (prefix before : , or null), degree (edge count), fact_count (total facts for this subject). Edge fields: id (fact id), from (subject entity ref), to (object entity ref), predicate , confidence . nodes and edges are the rendered subset; total_nodes and total_edges are the full graph counts before any limit is applied. limited is true when the graph was pruned to limit nodes. Example # Full graph (may be large) curl "$ABAGRAPH_BASE_URL/api/graph" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Focused on Alice and her neighborhood curl "$ABAGRAPH_BASE_URL/api/graph?q=alice&limit=40" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" GET /api/walk Query parameters Param Type Required Description start EntityRef string yes Starting entity for the BFS, e.g. Person:alice . depth integer no Maximum hops from start. direction string no out (default) | in | both . Controls edge traversal direction. edge_types CSV string no Comma-separated predicates to follow. Omit to traverse all predicates. as_of integer (Unix ms) no Time-travel: only traverse facts valid at this timestamp. Response { "start": "Person:alice", "reached": ["Person:alice", "Company:beta", "Team:platform"], "edges": [ { "id": "01J...", "from": "Person:alice", "to": "Company:beta", "predicate": "works_at", "fact": {"id": "01J...", "subject": "Person:alice", "predicate": "works_at", "object": "Company:beta", "confidence": 0.9, "status": "active"} }, { "id": "01J...", "from": "Person:alice", "to": "Team:platform", "predicate": "member_of", "fact": {"id": "01J...", "subject": "Person:alice", "predicate": "member_of", "object": "Team:platform", "confidence": 0.85, "status": "active"} } ] } Each edge carries id , from (subject), to (object), predicate , and fact — the full fact object (standard fact_view fields, no embedding). from and to always record the canonical fact direction (subject → object) regardless of the direction parameter. For inbound walks the traversal visits facts where the object is the start entity, but the edge still reads from =subject, to =object. Example # Walk outbound 2 hops from Alice, only 'works_at' and 'member_of' edges curl "$ABAGRAPH_BASE_URL/api/walk?start=Person:alice&depth=2&edge_types=works_at,member_of" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Walk inbound: find everyone who works at Company:beta curl "$ABAGRAPH_BASE_URL/api/walk?start=Company:beta&direction=in&edge_types=works_at" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" TypeScript SDK import { createClient } from '@abagraph/client' const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY }) // Graph overview const graph = await db.graph() // BFS walk const walk = await db.walk({ start: 'Person:alice', depth: 2, direction: 'out', edge_types: 'works_at,member_of', }) Notes Only facts with status: active contribute edges. Superseded and retracted facts are excluded unless as_of targets a time when they were active. Walk traverses only facts whose object resolves to an entity reference. Facts with numeric, boolean, blob, or null objects are skipped during traversal (they can still appear as subject nodes). Asserting a fact does not require an entity-ref object; object is optional in the general API. Walk edges always record from =subject, to =object regardless of the direction parameter. When walking inbound, the BFS enqueues the subject as the next node to visit, but the edge shape is unchanged. Tenant isolation: branch-tagged facts and other tenants' facts are filtered before any node or edge is built. API: /api/context — uses 1-hop walk internally for seed expansion API: Entities and Predicates — list all known entities API: /api/facts — raw fact listing without graph structure ### API Reference: Overview (api) API Reference: Overview TL;DR Base URL: https://<tenant>.abagraph.com . The managed service handles subdomain-per-tenant routing automatically. Auth: Authorization: Bearer <token> or abagraph_session cookie. Unauthorized returns 404 (not 401) to avoid advertising the gated surface. All errors: {"error":{"code":"…","message":"…"}} . Append ?explain=1 to any endpoint for a query-plan envelope. Base URL Your base URL is https://<tenant>.abagraph.com , where <tenant> is your tenant name. All API endpoints live under /api . The MCP dispatcher is at POST /mcp . curl -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ "https://<tenant>.abagraph.com/api/health" The managed service handles routing and durability for you. Set ABAGRAPH_BASE_URL in your shell or SDK config to avoid repeating the URL in every call. Authentication Bearer token The API always requires authentication. Every request must include the bearer token issued when your tenant was provisioned. Your token and base URL are shown in the abagraph console under Connect . curl -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ "$ABAGRAPH_BASE_URL/api/facts" Cookie session Exchange a token for a browser-friendly session cookie. The cookie carries the same key and is re-validated on every request — it does not replace Bearer auth, it wraps it. # Log in — sets abagraph_session cookie (HttpOnly, Secure, SameSite=Lax, 30-day TTL) curl -X POST "$ABAGRAPH_BASE_URL/api/session" \ -H "Content-Type: application/json" \ -d '{"key":"<your-token>"}' # Log out curl -X DELETE "$ABAGRAPH_BASE_URL/api/session" Response on success: {"scope":"tenant","tenant":"acme"} (or "scope":"admin" ). Subdomain routing Requests to your tenant subdomain (e.g. acme.abagraph.com ) are automatically scoped to tenant acme . A tenant token used against a different tenant's subdomain is rejected. The admin token spans all subdomains. Public endpoints These endpoints do not require authentication: GET /api/health POST /api/signup POST /api/session (the login door itself) Static UI files and /llms.txt , /openapi.json , and related machine-readable surfaces. Error shape All errors return JSON with a consistent envelope: {"error": {"code": "NotFound", "message": "no entity: Person:alice"}} Error codes Code HTTP status Meaning NotFound 404 Entity, fact, branch, or tenant does not exist. Conflict 409 Write violates Reject conflict policy; a conflicting active fact exists. CompareFailed 409 Compare-guarded /api/transact failed; body includes failures . SchemaInvalid 400 Missing required field, wrong type, or constraint violated. StorageCorruption 500 Storage layer returned an unexpected error. Io 500 I/O error (disk full, network timeout, etc.). Closed 503 Engine instance is shut down. MethodNotAllowed 405 HTTP method not valid for this path. Auth rejection returns 404, not 401. An unauthorized request to any /api/* endpoint returns 404 NotFound rather than 401. This is intentional — it avoids advertising the gated surface to unauthenticated scanners. Timestamps All Timestamp values (including valid_from , valid_until , recorded_at , as_of ) are Unix milliseconds ( i64 ). The engine clock ticks in milliseconds by default. # Current time in Unix milliseconds (macOS / Linux) date +%s%3N Query plan: ?explain=1 Append ?explain=1 to any request and the response is wrapped in a plan envelope: { "result": { /* normal response */ }, "explain": { "op": "query_facts", "plan": "subject index → filter active → limit 200", "stats": {"candidates_scanned": 1432, "results": 7, "limit_hit": false}, "steps": [{"phase": "execute", "ms": 1.2}], "total_ms": 2.1 } } Use ?explain=1 during development to understand index selection and identify slow queries. Do not use it in high-throughput production paths — it adds overhead. Tenant scoping Every read endpoint applies tenant_scope : facts that belong to a different tenant, or that are tagged as branch overlays, are silently dropped from the response. Admin callers see all tenants. A tenant-scoped token sees only its own facts — there is no opt-out. Content type All request bodies must be application/json . All responses are application/json except GET /api/stream (Server-Sent Events) and static assets. Request size Default body limit: 64 MiB ( ABAGRAPH_MAX_BODY_BYTES=67108864 ). Override with the environment variable before starting the server. Next steps API: /api/facts — assert and query facts API: /api/transact — atomic compare-and-swap batches API: /api/context — one-call agent context packet API: Stats and Health — liveness and telemetry ### API Reference: /api/search (api) API Reference: /api/search TL;DR GET or POST /api/search — embed a question and return the most similar facts by cosine distance. Requires the server to have been started with an embedding provider ( ABAGRAPH_EMBEDDING=hash or openai ). Facts must have been asserted while an embedding provider was active. Results are tenant-scoped: non-admin callers see only their own facts. Endpoint Method Path Auth GET /api/search Bearer / cookie (tenant-scoped) POST /api/search Bearer / cookie (tenant-scoped) Request GET: query string parameters. POST: JSON body. Field Type Default Description question string "" Natural-language query to embed and search. top_k integer null (engine default) Number of nearest-neighbour facts to return. as_of integer (Unix ms) null (now) Time-travel snapshot: only facts valid at this timestamp are candidates. Response { "kind": "search_result", "question": "Where does Alice work?", "answer_facts": [ { "id": "01J...", "subject": "Person:alice", "predicate": "works_at", "object": "Company:beta", "confidence": 0.95, "status": "active" } ], "entities": ["Person:alice", "Company:beta"] } GET example curl "$ABAGRAPH_BASE_URL/api/search?question=Where+does+Alice+work%3F&top_k=5" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" POST example curl -X POST "$ABAGRAPH_BASE_URL/api/search" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"question":"Where does Alice work?","top_k":5}' TypeScript SDK import { createClient } from '@abagraph/client' const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY }) const result = await db.search('Where does Alice work?') Embedding requirements Semantic search requires: The server started with ABAGRAPH_EMBEDDING=hash (deterministic, no API key) or ABAGRAPH_EMBEDDING=openai / http (real embeddings). Facts were asserted while an embedding provider was active. Facts asserted before a provider was configured have no embedding and are not returned by search. Empty results without error. If no embedding provider is configured, or no facts have embeddings, search returns an empty answer_facts array — not an error. Use GET /api/facts with explicit subject / predicate filters for pattern-based lookup instead. Environment variables Variable Description ABAGRAPH_EMBEDDING hash | openai | http . Omit to disable embeddings. ABAGRAPH_EMBEDDING_API_KEY API key for openai/http. Falls back to OPENAI_API_KEY . ABAGRAPH_EMBEDDING_BASE_URL Base URL for http mode. Default: https://api.openai.com/v1 . ABAGRAPH_EMBEDDING_MODEL Model name. Default: text-embedding-3-small . ABAGRAPH_EMBEDDING_DIM Dimension. Default: 64 (hash), 1536 (openai/http). API: /api/context — full context packet (uses search internally) API: /api/ask — search + BYOK model = grounded answer API: /api/facts — pattern-based fact lookup (no embedding required) ### API Reference: Stats and Health (api) API Reference: Stats and Health TL;DR GET /api/health — public liveness probe. No auth required. GET /api/stats — fact counts by status, entity count, predicate count, and server version. Tenant-scoped. Both support ?explain=1 for query-plan inspection. GET /api/health Public liveness probe. Returns immediately without touching storage. Use it for load-balancer health checks and readiness probes. Request curl "$ABAGRAPH_BASE_URL/api/health" Response (200) {"ok": true} A non-200 response indicates the server process has stopped. GET /api/stats Returns fact counts broken down by status, plus entity and predicate counts. Admin callers see the full graph; non-admin callers see only their tenant's facts. Request curl "$ABAGRAPH_BASE_URL/api/stats" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Response { "facts": { "active": 4231, "superseded": 812, "retracted": 47, "candidate": 19, "total": 5109 }, "entities": 342, "predicates": 18, "tenants": 1, "tenant": "acme", "version": "0.1.0" } Response fields Field Description facts.active Currently valid facts. facts.superseded Facts closed by a newer conflicting fact. Part of bitemporal history. facts.retracted Explicitly retracted facts. Excluded from queries by default. facts.candidate Facts below the confidence threshold. Do not supersede active facts. facts.total Sum of all statuses. entities Count of distinct entity refs (subjects) in the tenant. predicates Count of distinct predicate names. tenants Number of distinct tenants visible to the caller (1 for non-admin, N for admin). tenant Caller's tenant name, or "admin" for admin callers. version The server version. Notes GET /api/stats scans all facts in storage (tenant-filtered). For very large stores this may take a moment — avoid calling it in tight loops. Admin callers see cross-tenant counts. The tenants field reports the number of distinct tenant tags found in the scanned facts. Both endpoints support ?explain=1 . For /api/health the explain envelope reports a static plan. API: Overview — auth and public endpoint list API: /api/tenants — admin-level tenant management API: /api/facts — fact listing with full status filters ### API Reference: /api/stream (SSE) (api) API Reference: /api/stream (Server-Sent Events) TL;DR GET /api/stream — subscribe to a live fact feed. Every POST /api/facts , /api/transact , /api/digest , and connector ingestion publishes events here. Events are tenant-scoped: non-admin subscribers only see facts that belong to their tenant. A :keepalive comment is sent every 15 seconds to keep the connection alive through proxies. Endpoint Method Path Auth GET /api/stream Bearer / cookie (tenant-scoped) Response content type: text/event-stream . The connection stays open until the client disconnects. Event format Each event is a data: line followed by two newlines (standard SSE format): data: {"op":"assert","tx":107,"fact":{...}} data: {"op":"retract","tx":108,"fact":{...}} :keepalive Event fields Field Type Description op string "assert" or "retract" . tx integer Monotonic write-transaction number. Use tx as a cursor with GET /api/facts?tx_gt=<last_seen> to replay missed events. fact Fact object The full fact that was asserted or retracted. Example: curl curl -N "$ABAGRAPH_BASE_URL/api/stream" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" The -N flag disables curl's output buffering so events appear as they arrive. Example: browser EventSource const es = new EventSource('/api/stream', { headers: { 'Authorization': `Bearer ${apiKey}` }, }) es.onmessage = (event) = { const { op, tx, fact } = JSON.parse(event.data) console.log(op, fact.subject, fact.predicate, fact.object) } Browser EventSource and Authorization headers. The standard browser EventSource API does not support custom headers. Use the cookie session ( POST /api/session ) for browser-based SSE consumers, or a library that supports custom headers. Replay missed events The stream is in-memory: if the client disconnects it will miss events published during the gap. Use the tx cursor to replay: Before subscribing, call GET /api/stats (or any endpoint) and note the current tx . On reconnect, call GET /api/facts?tx_gt=<last_seen_tx> to fetch facts written while you were disconnected. Deduplicate with events from the new stream using the tx field. Notes The in-memory bus has no persistence and no replay. Events published before subscription are gone. Each subscriber holds a thread and a channel slot on the server. Avoid high subscriber counts in production — use a dedicated event broker for fan-out workloads. The server sends Cache-Control: no-cache and Connection: keep-alive on the SSE response. Intermediary proxies should be configured to pass SSE through without buffering. Branch facts are published to the SSE feed using the branch-overlay tenant. Non-admin subscribers scoped to the parent tenant will not see branch writes. API: /api/facts — poll-based alternative with ?tx_gt= cursor API: /api/transact — atomic batch writes that publish to this feed API: /api/connectors — connector ingestion events appear here ### API Reference: /api/tenants (api) API Reference: /api/tenants TL;DR Admin-only. All /api/tenants endpoints require the admin token. Create, list, get, and soft-delete tenant records. Soft-delete sets status=deleted ; the underlying facts persist until you hard-erase them with POST /api/admin/purge (targeted by entity kind / prefix / subject , not by tenant id). Tenant ids: ASCII alphanumeric or dash, ≤64 chars, no tilde ( ~ ). POST /api/tenants Register a new tenant (project). Returns 409 if a tenant with the same id is already active. Request body Field Type Required Description id string yes Unique tenant identifier. ASCII alnum/dash, ≤64 chars. name string no Human-readable display name. quota_facts integer no Soft fact-count quota (informational; not enforced by the engine). Response (201) { "id": "acme", "name": "Acme Corp", "status": "active", "quota_facts": 50000, "created_at": 1717200001234 } Example curl -X POST "$ABAGRAPH_BASE_URL/api/tenants" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"id":"acme","name":"Acme Corp","quota_facts":50000}' GET /api/tenants List all registered tenants with live fact counts. curl "$ABAGRAPH_BASE_URL/api/tenants" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Response: array of tenant rows, each with id , name , status , quota_facts , created_at , and fact_count (live active facts). GET /api/tenants/:id Get a single tenant by id, with live fact count. curl "$ABAGRAPH_BASE_URL/api/tenants/acme" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" DELETE /api/tenants/:id Soft-delete: sets the tenant's status to "deleted" . Facts are NOT erased — the tenant record is just flagged. Hard-erase the underlying data with POST /api/admin/purge (see below). curl -X DELETE "$ABAGRAPH_BASE_URL/api/tenants/acme" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Response: {"ok":true,"id":"acme","note":"soft-deleted; use /api/admin/purge to erase data"} Hard purge: /api/admin/purge Admin-only. Physically deletes facts for a selected entity set — no bitemporal history is kept, the rows are gone. The target is chosen by query parameter , not a request body; there is no "tenant id" purge. Provide exactly one of: Param Selects ?subject=<ref> One exact subject (e.g. Person:alice ). ?prefix=<str> Every subject starting with this arbitrary prefix. ?kind=<Kind> Every subject of this kind (prefix <Kind>: ). GET is a non-destructive dry-run preview — it returns how many facts a purge would erase and touches nothing. POST performs the erase and requires &confirm=1 so it cannot fire by accident; without it you get a 400 SchemaInvalid . # Preview (GET) — counts only, erases nothing curl "$ABAGRAPH_BASE_URL/api/admin/purge?prefix=Contact:" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # - {"prefix":"Contact:","facts":4,"dry_run":true} # Erase (POST) — requires &confirm=1 curl -X POST "$ABAGRAPH_BASE_URL/api/admin/purge?subject=Person:alice&confirm=1" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # - {"subject":"Person:alice","deleted":2,"dry_run":false} Omitting all three targets returns an error ( need ?kind= , ?prefix= or ?subject= ). Every successful POST emits an abagraph.admin.purge telemetry event. To wipe a soft-deleted tenant's data, purge by the prefix or kinds that tenant's subjects use. TypeScript SDK import { createClient } from '@abagraph/client' const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_API_KEY }) await db.tenants.create({ id: 'acme', name: 'Acme Corp' }) const list = await db.tenants.list() const tenant = await db.tenants.get('acme') await db.tenants.delete('acme') Errors Code HTTP Meaning Forbidden 403 Non-admin caller — admin scope required. SchemaInvalid 400 Invalid tenant id format. Conflict 409 Tenant already exists and is active. NotFound 404 Tenant does not exist. Notes Tenant ids may not contain a tilde ( ~ ) — that character is reserved for the internal branch-tenant encoding. Tenant records are stored as Tenant:<id> facts in the engine's own namespace. Listing tenants incurs a full storage scan. Subdomain routing maps tenant id to subdomain: tenant acme is served at acme.abagraph.com . API: Overview — auth, token scoping, subdomain routing API: /api/branches — per-tenant branch management API: Stats and Health — per-tenant fact counts ### API Reference: /api/transact (api) API Reference: /api/transact TL;DR POST /api/transact — assert and/or retract multiple facts atomically. Optional compare guards make it a compare-and-swap (CAS). Every guard is checked before any write. Mismatch on any guard → 409 CompareFailed ; nothing is written. Concurrent transactions that touch the same subject+predicate race. Exactly one wins; every loser gets the current state in the error. Endpoint Method Path Auth POST /api/transact Bearer / cookie (tenant-scoped) Request body { "compares": [ {"subject": "Person:alice", "predicate": "works_at", "expect": "Company:acme"}, {"subject": "Project:nova", "predicate": "status", "absent": true} ], "asserts": [ {"subject": "Person:alice", "predicate": "works_at", "object": "Company:beta", "source": "hr-system"} ], "retracts": ["01JXXXXXXXXXXXXXXXXXXXXXXX"] } Fields Field Type Required Description compares array no Zero or more compare guards evaluated before any write. asserts array of FactInput no* Facts to assert. Supersedes existing active facts per predicate (AutoSupersede by default). retracts array of string (fact ids) no* Fact ids to retract. Idempotent — retracting an already-retracted fact is a no-op. * At least one of asserts or retracts must be non-empty. Compare guards Each compare object must have subject + predicate , and exactly one of: Guard field Meaning "expect": <value> There must be exactly one active fact for this subject+predicate whose object equals expect . "absent": true There must be no active fact for this subject+predicate (create-if-absent guard). Response { "tx": 107, "facts": [ { "id": "01JXXXXXXXXXXXXXXXXXXXXXXX", "subject": "Person:alice", "predicate": "works_at", "object": "Company:beta", "status": "active", "tx": 107 } ], "retracted": [] } tx is the monotonic write-transaction number allocated for this batch. facts contains every fact touched by the batch (including superseded closes). retracted contains facts retracted by the batch. Errors Code HTTP Meaning CompareFailed 409 One or more compare guards did not match. Body includes failures with the current active state for each failed guard. Conflict 409 An assert used policy: "reject" and a conflicting fact is already active. SchemaInvalid 400 Malformed body; missing subject or predicate ; no asserts or retracts. CompareFailed response body { "error": { "code": "CompareFailed", "message": "compare failed: 1 guard(s) did not match current state", "failures": [ { "subject": "Person:alice", "predicate": "works_at", "expected": "Company:acme", "current": ["Company:beta"] } ] } } Example: atomic move curl -X POST "$ABAGRAPH_BASE_URL/api/transact" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "compares": [ {"subject":"Person:alice","predicate":"works_at","expect":"Company:acme"} ], "asserts": [ {"subject":"Person:alice","predicate":"works_at","object":"Company:beta","source":"hr-transfer"} ] }' TypeScript SDK import { createClient } from '@abagraph/client' const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL, apiKey: process.env.ABAGRAPH_KEY }) const result = await db.transact({ compares: [{ subject: 'Person:alice', predicate: 'works_at', expect: 'Company:acme' }], asserts: [{ subject: 'Person:alice', predicate: 'works_at', object: 'Company:beta', source: 'hr-transfer' }], }) Notes The managed service applies each batch as a single atomic, consistent step — guard evaluation and write are guaranteed to be consistent. PII in asserted facts is scrubbed (redacted) before storage, same as POST /api/facts . Non-admin callers have their tenant stamped on every compare and assert; the body's own tenant field is ignored. A successful transact publishes all new facts to the SSE feed ( GET /api/stream ). API: /api/facts — single-fact assert/retract API: Stream (SSE) — observe facts as they land Guide: Assert a fact ### CLI Command Reference (cli) CLI Command Reference TL;DR All commands: init , assert , query , branch , connect , factcheck , agent init , consolidate , help . Global flags --url and --key override env vars and ~/.abagraph.json on any command. All output goes to stdout as JSON (2-space indent); errors go to stderr; exit code 1 on failure. Global flags These flags are accepted by every command and take precedence over environment variables and the config file: Flag Environment variable Description --url <URL> ABAGRAPH_URL Base URL of the abagraph instance (no trailing slash) --key <KEY> ABAGRAPH_KEY API key sent as Authorization: Bearer <KEY> init Synopsis npx abagraph init [--url <URL>] [--key <KEY>] Description Writes the resolved URL and key to ~/.abagraph.json , then verifies connectivity by calling GET /api/health and prints the REST, MCP (Model Context Protocol), and Server-Sent Events stream endpoints from GET /api/connect . Run init once per machine or environment. Subsequent commands pick up the saved config automatically. Flags Flag Required Description --url <URL> Yes (on first run) Base URL of the abagraph instance --key <KEY> Yes API key sent as Authorization: Bearer <KEY> Example npx abagraph init --url https://your-abagraph-host --key <YOUR_API_KEY> Output: Wrote ~/.abagraph.json Health: OK REST: https://your-abagraph-host/api MCP: https://your-abagraph-host/mcp Stream: https://your-abagraph-host/api/stream Saved in plain text. The API key is written to ~/.abagraph.json as a plain string. Restrict file permissions ( chmod 600 ~/.abagraph.json ) or prefer the ABAGRAPH_KEY environment variable in shared or CI environments. assert Synopsis npx abagraph assert <subject> <predicate> <object> [--branch <B>] Description Writes a single fact to the graph by calling POST /api/facts . The three positional arguments map directly to a FactInput : subject — an entity reference in Type:identifier format (for example, user:alice or Person:alice ) predicate — the relationship name (for example, prefers , works_at ) object — the value, sent as a string; the server interprets it as a Text fact object If a fact with the same subject and predicate already exists and the conflict policy is AutoSupersede (the default), the existing fact is atomically closed and the new one becomes Active . To write a second value for the same predicate without superseding, use the REST API directly with conflict_policy: "Coexist" . Flags Flag Description --branch <B> Write the fact onto a named branch overlay instead of main Example npx abagraph assert user:alice prefers dark-mode { "id": "01J0000000000000000000000A", "subject": "user:alice", "predicate": "prefers", "object": "dark-mode", "valid_from": 1750000000.0, "valid_until": null, "recorded_at": 1750000000.0, "confidence": 1.0, "source": null, "properties": null, "status": "Active", "tenant": null, "transient": false, "tx": 1 } Write onto a branch: npx abagraph assert user:alice prefers light-mode --branch experiment query Synopsis npx abagraph query [--subject <S>] [--predicate <P>] [--limit <N>] [--branch <B>] Description Lists facts by calling GET /api/facts with the supplied query parameters. All flags are optional; omitting all of them returns up to the server default (200) most recent facts. Flags Flag Description --subject <S> Filter by subject (for example, user:alice ) --predicate <P> Filter by predicate name --limit <N> Maximum number of facts to return (server default: 200) --branch <B> Read from a named branch overlay Example npx abagraph query --subject user:alice --limit 5 [ { "id": "01J0000000000000000000000A", "subject": "user:alice", "predicate": "prefers", "object": "dark-mode", "valid_from": 1750000000.0, "valid_until": null, "recorded_at": 1750000000.0, "confidence": 1.0, "source": null, "properties": null, "status": "Active", "tenant": null, "transient": false, "tx": 1 } ] branch The branch command manages write-isolated branch overlays. A branch is an instant fork of the fact store at a point in time. Facts written to a branch do not affect main . Branches can be diffed, promoted to main, or discarded. Branch names must be ASCII alphanumeric with hyphens, at most 64 characters, and cannot be main . branch create npx abagraph branch create <name> [--parent <P>] [--as-of <MS>] Flag Description --parent <P> Name of the parent branch to fork from (defaults to main ) --as-of <MS> Pin the branch to this point in time — Unix epoch milliseconds npx abagraph branch create experiment --parent main npx abagraph branch create snapshot --as-of 1718800000000 branch list npx abagraph branch list Returns a JSON array of all branches. branch diff npx abagraph branch diff <name> Returns the set of facts that differ between the named branch overlay and its base. Useful for reviewing changes before promotion. branch promote npx abagraph branch promote <name> Merges the branch overlay into main and marks the branch as promoted. Facts on the branch that conflict with main are resolved via the standard supersession rules. branch discard npx abagraph branch discard <name> Removes the branch overlay without merging. The base facts are unaffected. Branch workflow example # Isolate an experiment npx abagraph branch create experiment # Write to the branch npx abagraph assert user:alice prefers light-mode --branch experiment # Review the difference from main npx abagraph branch diff experiment # Accept and merge npx abagraph branch promote experiment # Or throw it away # npx abagraph branch discard experiment connect Synopsis npx abagraph connect Description Calls GET /api/connect and prints the connection snippets as JSON. The snippets object includes ready-to-paste code for curl, TypeScript, CLI, agent memory, the memory plugin, and an MCP config block. The API key is never echoed back as a real value — the server always uses <YOUR_API_KEY> as a placeholder. Example npx abagraph connect { "curl": "curl -H 'Authorization: Bearer <YOUR_API_KEY>' https://your-host/api/facts", "ts": "import { createClient } from '@abagraph/client'; ...", "cli": "npx abagraph query --subject ...", "agent": "import { createAgentMemory } from '@abagraph/client/agent'; ...", "plugin": "import { memoryPlugin } from '@abagraph/client/plugin'; ...", "mcp_config": { "mcpServers": { "abagraph": { ... } } } } factcheck Synopsis npx abagraph factcheck [--subject <S>] [--predicate <P>] Description Calls GET /api/factcheck and reports contradictions — pairs of active facts with the same subject and predicate that should not coexist. The command prints a summary line followed by the contradictions array as JSON. Flags Flag Description --subject <S> Scope the check to a single subject --predicate <P> Scope the check to a single predicate Example npx abagraph factcheck --subject user:alice --predicate prefers contradictions: 1 [ { "subject": "user:alice", "predicate": "prefers", "facts": [ { "id": "01J...", "object": "dark-mode", ... }, { "id": "01J...", "object": "light-mode", ... } ] } ] agent init Synopsis npx abagraph agent init Description Fetches connection snippets from GET /api/connect and prints the agent memory and plugin code snippets to stdout. It also writes an MCP server configuration to ./abagraph.mcp.json in the current directory, which you can point your MCP-compatible agent runner at. Output --- agent snippet (createAgentMemory) --- import { createAgentMemory } from '@abagraph/client/agent'; const memory = createAgentMemory({ baseUrl: '...', apiKey: '...', namespace: '...' }); --- plugin snippet (memoryPlugin) --- import { memoryPlugin } from '@abagraph/client/plugin'; runner.use(memoryPlugin({ baseUrl: '...', apiKey: '...', namespace: '...' })); Wrote ./abagraph.mcp.json The generated abagraph.mcp.json contains the MCP server configuration pointing to the managed endpoint, ready to use with Claude Desktop or any MCP-compatible runner. consolidate Synopsis npx abagraph consolidate Description Calls POST /api/consolidate to deduplicate persisted facts and surface contradictions. Duplicate facts (same subject, predicate, and object) are collapsed into one; conflicting facts that should supersede each other are resolved. Use this after bulk imports or connector syncs to tidy the store. Example npx abagraph consolidate collapsed: 3 superseded: 1 conflicts: 0 { "collapsed": 3, "superseded": [ "01J...", "01J..." ], "conflicts": 0 } The summary line is printed first, followed by the full JSON response. collapsed is the count of deduplicated facts; superseded is the list of fact IDs that were closed; conflicts is the count of unresolvable contradictions surfaced. help Synopsis npx abagraph help npx abagraph --help Description Prints the built-in usage summary listing all commands and global flags, then exits with code 0. Common mistakes Missing URL Every command that calls the API exits with No abagraph URL configured if no URL is set. Run npx abagraph init --url <URL> or export ABAGRAPH_URL . Object values sent as strings The assert command sends the object argument as a plain JSON string. The server interprets it as a Text fact object. To assert a numeric, boolean, or entity-reference object, use the TypeScript SDK or the REST API directly. Branch --as-of expects milliseconds The --as-of flag on branch create expects Unix epoch milliseconds , not seconds: # Correct: milliseconds npx abagraph branch create snapshot --as-of 1718800000000 # Wrong: this is epoch seconds — the branch will be anchored in 2025, not 2024 npx abagraph branch create snapshot --as-of 1718800000 CLI Overview and Install — installation and configuration reference API Reference: /api/facts — the REST surface assert and query call Getting Started: Quickstart — broader first-run guide with server setup ### CLI Overview and Install (cli) CLI Overview and Install TL;DR Run without installing: npx abagraph <command> (Node 18+, zero extra dependencies). Save config once with npx abagraph init --url <URL> --key <KEY> . Config lookup order: flag > environment variable > ~/.abagraph.json . What the CLI is The abagraph CLI ( @abagraph/cli , bin name abagraph ) is a zero-dependency Node.js script that communicates with an abagraph HTTP server over the REST API. It lets you write and read facts, manage branches, check for contradictions, and bootstrap agent tooling from the terminal — without writing Rust or TypeScript. The package ships one file ( bin/abagraph.mjs ) and uses only Node built-ins plus the built-in fetch global introduced in Node 18. There are no node_modules to install when you run via npx . Requirements Node.js 18 or later An abagraph API endpoint — your base URL and API key are shown in the abagraph console under Connect An API key issued from the abagraph console under Connect Installation Run without installing via npx This is the recommended approach. npx downloads and executes the latest published version on demand: npx abagraph help No install step is required. The package is fetched from the npm registry and cached by npx . Global install npm install -g @abagraph/cli abagraph help After a global install the abagraph command is available directly on your PATH without the npx prefix. Install in a project npm install --save-dev @abagraph/cli # abagraph is now in node_modules/.bin; npx resolves it locally npx abagraph help Configuration Every command that makes an HTTP request needs two values: the base URL of your abagraph instance and an optional API key . Both are looked up in the following order — the first match wins: A command-line flag ( --url / --key ) An environment variable ( ABAGRAPH_URL / ABAGRAPH_KEY ) The user-level config file ~/.abagraph.json Setting Flag Environment variable Config file field Base URL --url <URL> ABAGRAPH_URL url API key --key <KEY> ABAGRAPH_KEY key Config file format The config file lives at ~/.abagraph.json . Create it with abagraph init (see below) or write it by hand: { "url": "https://your-abagraph-host", "key": "<YOUR_API_KEY>" } A malformed config file is silently ignored; the CLI falls back to environment variables. Quickstart Point the CLI at your abagraph endpoint and save the config. Your base URL and API key are shown in the abagraph console under Connect . npx abagraph init --url https://your-tenant.abagraph.com --key <YOUR_API_KEY> Wrote ~/.abagraph.json Health: OK REST: https://your-tenant.abagraph.com/api MCP: https://your-tenant.abagraph.com/mcp Stream: https://your-tenant.abagraph.com/api/stream Write and read back a fact: npx abagraph assert user:alice prefers dark-mode npx abagraph query --subject user:alice --limit 10 Output format Every command writes JSON to stdout , pretty-printed with two-space indentation. Some commands also write a human-readable summary line to stdout before the JSON (for example, factcheck prints contradictions: N and consolidate prints a one-line summary). Error messages are always written to stderr , and the process exits with code 1 . Error handling When the server returns a non-2xx response the CLI extracts the structured error and exits: NotFound: fact 01J... not found The format is <code>: <message> matching the server's { "error": { "code": "...", "message": "..." } } shape. See the Errors reference for the full list of error codes. Missing URL error If no base URL is configured, every command exits immediately with: No abagraph URL configured. Run `abagraph init --url <URL>` or set ABAGRAPH_URL. Using in CI and scripts For non-interactive environments set the environment variables rather than using init : export ABAGRAPH_URL=https://your-abagraph-host export ABAGRAPH_KEY=<YOUR_API_KEY> npx abagraph assert pipeline:build-42 status success npx abagraph query --subject pipeline:build-42 The API key is sent as Authorization: Bearer <KEY> on every request. CLI Command Reference — complete flag and argument reference for every command Getting Started: Quickstart — first-run guide including server setup API Reference: /api/facts — the REST surface the CLI calls ### Architecture (concepts) Architecture TL;DR Use this page when you want to understand why abagraph behaves the way it does. Five strict layers: primitives → core algorithms → storage → orchestration → surfaces . The core layer contains pure algorithms (no I/O, no side effects) — this is why conflict resolution and time-travel are deterministic and predictable. Embedding providers are ports : swap the provider by changing an environment variable, not by changing the engine. All four API surfaces share one storage layer — no copies, no sync, no inconsistency. The layer map ┌─────────────────────────────────────────────────────┐ │ surfaces REST API · MCP "orchestrate" · agent │ │ kernel · wiki ingestion · console UI │ ├─────────────────────────────────────────────────────┤ │ orchestration wires core algorithms to a │ │ storage backend + embedding service │ ├─────────────────────────────────────────────────────┤ │ storage durable fact log + inverted indexes │ │ (tenant / subject / predicate / object) │ ├─────────────────────────────────────────────────────┤ │ core pure algorithms — supersede, query, │ │ walk, knn, match, build_fact │ ├─────────────────────────────────────────────────────┤ │ primitives fact · entity · pattern · config · err │ └─────────────────────────────────────────────────────┘ What each layer may do Layer May depend on Must never primitives nothing contain logic beyond plain data core primitives do I/O, touch globals, know about storage storage primitives embed business rules or mutate indexes on its own orchestration all below reimplement core algorithms inline surfaces orchestration reach past it into storage directly Why the core is pure The core layer contains no I/O and no global state. Every algorithm — conflict detection, bitemporal validity filtering, graph walking, kNN search — is a pure function over the fact types. This is why behaviour is predictable: the same inputs always produce the same outputs, regardless of which storage backend is in use or whether an embedding service is configured. Port-based extensibility Two services plug into the orchestration layer as ports: Embedding service — selected via ABAGRAPH_EMBEDDING . The engine calls it at assert time to produce a float vector stored alongside the fact. Swap the provider by changing the environment variable — no engine changes needed. See Embeddings and Search for configuration options. Storage backend — the managed hosted service uses a durable, replicated log. Dedicated deployments can select the backend through deployment configuration. Neither port contains business rules. All conflict resolution, tenant isolation, and time-travel logic stays in the pure core where it can be reasoned about independently. One storage layer, four surfaces All four API surfaces — Facts REST, schema'd entities, the agent kernel, and the MCP tools — read and write the same underlying storage. There is no duplication and no synchronisation step between them. A fact asserted via POST /api/facts is immediately visible to recall over MCP, GET /api/entities/… , and the agent kernel's context assembly on the very next turn. Load-bearing invariants Immutable writes only. Once a fact is written, it is never mutated in place. A contradiction produces a new fact and closes the old one atomically. WHY: time-travel, audit, and branching all depend on every past state being preserved exactly as it was written. Stdout purity (MCP). The stdio MCP transport carries JSON-RPC on stdout. Any non-protocol bytes on stdout corrupt the connection — all logging goes to stderr. Conflict resolution is tenant-local. The isolation boundary for conflict detection is the tenant. A fact in one namespace never supersedes a fact in another, even on identical subject and predicate. This invariant is not configurable. The Four Surfaces — which surface to use for which job Conflict and Supersession — the pure algorithm behind atomic writes Tenancy and Isolation — the tenant isolation chokepoint Embeddings and Search — the embedding service port ### Bitemporal Time (concepts) Bitemporal Time TL;DR Valid time ( valid_from / valid_until ): when the claim was true in the world . Record time ( recorded_at ): when your system learned the claim. Adding ?as_of=<timestamp_ms> to a query returns a read-only snapshot of the world as it was known at that millisecond. Timestamps are i64 milliseconds since the Unix epoch. Why two timelines? Single-timeline systems only track when data was written . That is enough for many databases but catastrophic for agent memory. An agent that made a decision in January needs you to reconstruct what it believed in January — not what you happened to write in the database at that moment. Suppose Alice changed jobs in January, but your agent only found out in March. With a single timestamp you cannot distinguish those two facts. With two timelines: Valid time records that Alice left her old job in January. Record time records that your system learned about the change in March. So as_of(February) returns the honestly-held, honestly-wrong belief — exactly what you need when auditing why an agent did what it did in February. Valid time Valid time is the interval during which a fact was true in the world. It is modelled as a half-open interval [valid_from, valid_until) : Field Meaning valid_from Start of validity. Defaults to now() when omitted in FactInput . valid_until End of validity. None means the fact is still open-ended (currently true). When a contradicting fact is asserted, the engine closes the old fact: it sets valid_until to the new fact's valid_from and flips the status to Superseded . That write happens atomically in one batch — you never see a moment where both are Active. Record time recorded_at is always the wall-clock time of the write. It is set by the engine, not the caller. You cannot back-date it. This asymmetry is deliberate: valid time can be back-dated (you can record that something was true last Tuesday), but you cannot pretend you knew something before you did. The as_of view Pass ?as_of=<timestamp_ms> on any query endpoint ( /api/facts , /api/walk , /api/search ) to bind the read to that millisecond. The service filters on valid time: a fact is included when valid_from <= t < valid_until (or valid_until is absent, meaning it was still open at t ). # Step 1: assert first fact curl -X POST $ABAGRAPH_BASE_URL/api/facts \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:alice","predicate":"works_at","object":"Org:acme"}' # Note the timestamp T here (milliseconds since Unix epoch) # Step 2: assert superseding fact — Org:acme is now Superseded curl -X POST $ABAGRAPH_BASE_URL/api/facts \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:alice","predicate":"works_at","object":"Org:beta"}' # Present query: returns only Org:beta (Active) curl "$ABAGRAPH_BASE_URL/api/facts?subject=Person:alice&predicate=works_at" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Time-travel: what was Alice's employer at time T? curl "$ABAGRAPH_BASE_URL/api/facts?subject=Person:alice&predicate=works_at&as_of=T" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # → [{ "object": "Org:acme", "status": "Superseded", … }] as_of and Superseded facts An as_of query includes Superseded facts whose validity window covered the requested timestamp. A live query (no as_of ) returns only Active facts. The status field in the returned fact reflects the fact's current status, not its status at the queried time — a fact may be Superseded in the result if it was active at t but has since been closed. Back-dating valid time You can back-date the validity window of a fact you are asserting now. This is useful when you learn about an event that happened in the past: # "Alice joined Acme in January 2024" — recorded today, valid from then curl -X POST $ABAGRAPH_BASE_URL/api/facts \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subject": "Person:alice", "predicate": "worked_at", "object": "Org:acme", "valid_from": 1704067200000, "valid_until": 1711929600000, "source": "hr-export" }' # valid_from = 2024-01-01T00:00:00Z, valid_until = 2024-04-01T00:00:00Z Trade-offs Richer history, more storage. Every superseded fact is kept. Use transient: true with a valid_until if you genuinely want TTL-style expiry. Back-dating can surprise readers. If you insert a fact with a past valid_from , callers querying as_of that past time will now see it — even though they queried before you wrote. This is correct bitemporal behaviour, not a bug. Record time is immutable. You cannot amend recorded_at . The "when we learned it" axis is always honest. Facts and Triples — all Fact fields in detail Conflict and Supersession — how valid_until is set on close Mental Model — the conceptual introduction Guide: Assert a Fact — time-travel code walkthrough ### Branches (concepts) Branches TL;DR A branch is an instant fork of the current fact store at a named point — no data is copied. Branch writes land in a derived namespace (the overlay) and never affect facts on main . Reading a branch returns the reconciled view : overlay wins over snapshot on the same (subject, predicate). Promote a branch to merge its overlay into main; discard to drop it. What is a branch? A branch is an overlay over an as_of snapshot of main. Creating a branch is instantaneous — abagraph does not copy any facts. Instead, it records the branch in the fact registry and tags all subsequent branch writes with a derived tenant and a _branch marker in their properties. This is the bitemporal answer to Neon's branching and point-in-time restore: the snapshot is inherent in the fact model (every fact carries its validity window), so a branch is just an overlay namespace on top of a timestamp-bounded read. Branch naming Branch names must follow these rules: ASCII alphanumeric characters, hyphens ( - ), and underscores ( _ ) only. Maximum 64 characters. The name main is reserved and cannot be used as a branch name. The characters : and ~ are reserved namespace separators and are not allowed. Valid examples: feature-pii-masking , experiment_1 , staging . Invalid: main , feat:new , a~b . How branches work internally When you write a fact to branch feat owned by tenant acme : The engine creates a derived tenant : "acme~feat" . Facts written on the branch carry tenant = "acme~feat" and properties._branch = "feat" . The derived tenant uses the ~ character as a namespace separator — this is why tenant IDs cannot contain ~ . A fact on the derived tenant never supersedes a fact on the parent tenant, because conflict resolution is tenant-local. When you read from a branch, the engine reconciles the overlay with the snapshot: it merges branch-derived facts with the parent facts that are not shadowed by the overlay on the same subject and predicate. Overlay wins on any (subject, predicate) pair that appears in both. REST API # Create a branch curl -X POST $ABAGRAPH_BASE_URL/api/branches \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "experiment-1"}' # Query facts on the branch curl "$ABAGRAPH_BASE_URL/api/facts?branch=experiment-1" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Assert a fact to the branch curl -X POST $ABAGRAPH_BASE_URL/api/facts \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:alice","predicate":"works_at","object":"Org:new","branch":"experiment-1"}' # Diff branch vs. main curl "$ABAGRAPH_BASE_URL/api/branches/experiment-1/diff" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Promote to main curl -X POST $ABAGRAPH_BASE_URL/api/branches/experiment-1/promote \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Discard curl -X POST $ABAGRAPH_BASE_URL/api/branches/experiment-1/discard \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" The reconcile algorithm Reading a branch produces a view that is the union of two sets, with the overlay taking priority: Collect overlay facts : all active facts with the derived branch tenant. Collect the snapshot set : active parent-tenant facts at as_of the branch creation time. Remove from the snapshot set any (subject, predicate) pair that appears in the overlay. Return overlay + filtered snapshot. The reconcile algorithm is deterministic: given the same overlay and snapshot, it always produces the same merged view. Diff and promote The diff endpoint compares the branch overlay against the parent snapshot and returns three lists: added — (subject, predicate) pairs that exist on the branch but not in the snapshot. changed — same (subject, predicate) but different object. removed — overlay facts with status Retracted (explicit deletions on the branch). Promote applies the diff to main: overlay facts become main facts (the _branch marker is stripped from their properties); retracted branch facts produce retractions on main. After promotion the branch is automatically discarded. Isolation guarantees Branch writes never appear on main reads (the derived tenant is excluded by the normal tenant scope filter, which excludes any tenant containing ~ ). Branch reads see a consistent snapshot of main as it was at branch creation, plus the branch overlay — never partially-applied changes from another branch. Tenancy and Isolation — how the derived tenant keeps branches isolated Bitemporal Time — the as_of snapshot that backs every branch Conflict and Supersession — tenant-local conflict scope ### Candidates and Confidence (concepts) Candidates and Confidence TL;DR Every fact carries a confidence score between 0.0 and 1.0. Facts asserted below the threshold (default 0.75 ) land as Candidate — stored and searchable, but they do not supersede anything. Reassert the same triple at full confidence to promote a Candidate to Active. This protects high-quality memory from being silently overwritten by low-quality input. The confidence field Every Fact has a confidence: f64 in the range 0.0 – 1.0 . It represents how certain you are that the claim is true. When you omit confidence in a FactInput , the engine defaults it to 1.0 (fully confident). Confidence serves two purposes: Conflict resolution guard — low-confidence facts cannot supersede high-quality memory. Context ranking — the context packet ( POST /api/context ) ranks facts by confidence when trimming to a token budget. The candidate threshold The default threshold is 0.75 . For dedicated deployments this threshold is configurable via deployment settings. When a fact's confidence is strictly below the threshold, the engine assigns FactStatus::Candidate instead of FactStatus::Active — regardless of the conflict policy in force. Candidate behaviour Property Active Candidate Visible in default queries Yes Filtered by default (request status=candidate explicitly) Participates in conflict resolution Yes No — never supersedes Returned by semantic search Yes Yes (embeds at write time) Visible via as_of Yes Yes Can be promoted N/A Yes — reassert at confidence ≥ threshold Candidates are not drafts you throw away. They are permanently stored and searchable. The distinction is that they cannot overwrite established memory. Do not use them as a scratchpad — use transient: true with a valid_until if you want expiring scratch data. Asserting a Candidate Confidence 0.4 is below the default threshold of 0.75, so this fact lands as Candidate : curl -X POST $ABAGRAPH_BASE_URL/api/facts \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subject": "Person:alice", "predicate": "works_at", "object": "Org:unknown-startup", "confidence": 0.4, "source": "unverified-twitter" }' # Response: { ..., "status": "Candidate" } Promoting a Candidate Once you have verified the claim, reassert it at or above the threshold. The engine detects that the existing Candidate fact has the same subject, predicate, object, and source, and promotes it to Active : curl -X POST $ABAGRAPH_BASE_URL/api/facts \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subject": "Person:alice", "predicate": "works_at", "object": "Org:unknown-startup", "confidence": 0.95, "source": "verified-linkedin" }' # Response: { ..., "status": "Active" } Confidence in context packets When the context endpoint or MCP build_context tool trims facts to fit a token budget, it ranks by confidence first. A fact with confidence 0.95 is more likely to survive a tight budget than one with 0.6 . This means that investing in good confidence scores pays off at inference time, not just at write time. Confidence in digest The POST /api/digest endpoint (and MCP digest tool) runs a BYOK judge model that assigns confidence scores before asserting. The threshold parameter lets you decide how high the bar must be for a fact to be written as Active vs. Candidate. Facts below the threshold are returned in the candidates field of the response and stored as Candidate in the engine — they do not overwrite existing memory. Related Facts and Triples — FactStatus enum Conflict and Supersession — why Candidates cannot supersede Context Packets — how confidence drives ranking ### Conflict and Supersession (concepts) Conflict and Supersession TL;DR Asserting a new value for a functional predicate atomically closes the old fact and opens the new one. The closed fact becomes Superseded — its history is preserved, not deleted. Three policies per assert: AutoSupersede (default), Coexist (set-valued), Reject . Conflicts are tenant-local : an assert in one namespace never supersedes another namespace's fact. The problem Agents learn things that contradict earlier beliefs. Alice no longer works at Acme — she joined Beta. A naive database would overwrite the old row. abagraph does not overwrite. Instead, it supersedes : the old fact is closed (its valid_until is set and status becomes Superseded ) and a new active fact is opened — all in a single atomic batch. History is data, not noise. How supersession works When you assert a fact, the engine runs conflict detection: Find all currently Active facts with the same (subject, predicate, tenant) . If the new fact is an idempotent duplicate (same object, same source, same confidence within 0.001), return the existing fact — no write needed. If there are no conflicts, write the new fact and return. If there are conflicts, apply the ConflictPolicy : Conflict policies Policy Behaviour When to use AutoSupersede Close all conflicting facts; write the new one. Atomic batch. Default. Functional predicates: works_at , status , any one-value-per-subject claim. Coexist Write the new fact alongside existing ones — no close, no conflict check. Set-valued predicates: tag , references , cites , mentions . Reject Refuse the new fact; return AbagraphError::Conflict with the IDs of the blocking facts. When you want the caller to handle the conflict explicitly (optimistic-lock style). The engine's default is AutoSupersede . Override it per-assert via the policy_override field in the request body: # Tag is set-valued — keep all tags active (Coexist) curl -X POST $ABAGRAPH_BASE_URL/api/facts \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Document:spec","predicate":"tag","object":"rust","policy_override":"coexist"}' # Explicit reject — let the caller handle the conflict curl -X POST $ABAGRAPH_BASE_URL/api/facts \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:alice","predicate":"works_at","object":"Org:beta","policy_override":"reject"}' # → 409 Conflict with the IDs of the blocking facts if a conflict exists The atomic close Under AutoSupersede , the engine produces a single atomic write that contains both the close of the old fact (setting valid_until = new_fact.valid_from and status = Superseded ) and the write of the new fact. The whole batch is committed in one operation — there is no window where both facts are Active or neither is. Tenant-local conflict resolution Conflict detection is scoped to the tenant . The engine filters active facts by fact.tenant == new_fact.tenant before looking for conflicts. This means: A fact in tenant "acme" never supersedes a fact in tenant "beta" , even if they share the same subject and predicate. None tenant is the global scope — global facts conflict with other global facts only. This is the single tenant-isolation chokepoint for writes — enforced unconditionally before any conflict check runs. Compare-and-swap (transact) For optimistic concurrency you can use POST /api/transact with a compares precondition array. The engine checks that all compare conditions hold before applying any asserts or retracts. On a mismatch it returns 409 CompareFailed with the failing conditions. curl -X POST $ABAGRAPH_BASE_URL/api/transact \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "compares": [{"subject":"Person:alice","predicate":"works_at","object":"Org:acme"}], "asserts": [{"subject":"Person:alice","predicate":"works_at","object":"Org:beta"}], "retracts": [] }' Idempotent duplicate detection Before applying any policy, the engine checks whether the incoming fact is a duplicate of an existing active fact (same object, same source, same confidence within 0.001). If so, it returns the existing fact without writing anything — asserting the same claim twice is safe and cheap. Facts and Triples — FactStatus enum Candidates and Confidence — facts that do not supersede Bitemporal Time — how valid_until is set on close Tenancy and Isolation — tenant scoping in depth ### Context Packets (concepts) Context Packets TL;DR A context packet is everything an agent should know before starting a task — delivered in one call. It combines semantic search (kNN recall), graph walk, wiki pages, and corroboration signals. The packet is trimmed to a token budget by confidence-ranking the candidate facts. Two shapes: ContextPacket (REST/Fact API) and the turn context packet (agent kernel). Why context packets? An agent that needs to brief itself before a task would otherwise have to issue several separate queries: search for relevant facts, walk the entity graph, fetch wiki pages, deduplicate, rank by relevance, and trim to a token limit. A context packet does all of that in one call and returns a single structured object the agent can inject directly into its prompt. ContextPacket fields Returned by GET|POST /api/context : Field Type Description summary String One-paragraph prose summary of the assembled context. entities Vec<EntityRef> Entity IDs encountered during the walk. facts Vec<Fact> The kept facts after ranking and trimming to token_budget . pages Vec<PageSnippet> Wiki page snippets adjacent to seed entities. tokens usize Estimated tokens in the kept set. total_tokens usize Estimated tokens in the full candidate set before trimming. collapsed usize Deduplication count — how many near-duplicate facts were collapsed. corroboration Vec<Corroboration> Per-kept-fact consensus signal: how many sources agree vs. conflict. derived_facts Vec<Fact> Facts synthesised from the dropped tail via BYOK tail-compaction. Empty on the default offline path. warnings Vec<ContextWarning> Non-fatal issues (e.g. no embedding provider configured). explain ContextExplain Assembly trace — phases and steps (also available via ?explain=1 ). Context request parameters curl -X POST $ABAGRAPH_BASE_URL/api/context \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "goal": "Summarise Alice'\''s work history", "seeds": ["Person:alice"], "token_budget": 4096, "max_facts": 32, "as_of": null, "cluster_threshold": 0.85 }' Parameter Default Description goal — The task description. Used as the kNN query string. seeds [] Entity IDs to start the graph walk from. token_budget — Trim the fact set to approximately this many tokens. max_facts 32 Hard cap on number of returned facts. as_of null (current) Millisecond timestamp for a historical snapshot. cluster_threshold — Cosine similarity threshold for near-duplicate collapse. compact_model — BYOK model for tail-compaction (BYOK path only). project — Filter to a specific project/tenant. Turn context packet (agent kernel) The agent kernel uses a different packet shape designed for per-turn briefing inside a durable loop: Field Description run The decoded run summary (budget, status, trigger). spec The decoded spec (role, goal, model, tool scope). prior_steps_summary Compressed summary of steps older than the recent window. recent_steps The last max_recent_steps (default 6) steps verbatim. recalled_facts kNN facts recalled from the Fact store using the current turn query. walked_entities Entities reached by walking from the seeds extracted from recent steps. pages Wiki page snippets adjacent to seeds and walked entities. warnings Assembly warnings (e.g. turn requested but no steps written yet). explain Assembly trace. The kernel assembles this packet on every turn and passes it to the model. The default token_budget for a turn is 2048 tokens. MCP build_context The MCP build_context tool produces a packet equivalent to the REST context endpoint for use in an LLM tool chain: { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "build_context", "arguments": { "task": "Summarise Alice's work history", "namespace": "acme", "seeds": ["Person:alice"], "max_facts": 32, "max_tokens": 4096 } }, "id": 1 } The default max_tokens for build_context is 4096 ; the maximum accepted value is 100000 . The Agent Kernel — how the turn context packet is used in the loop Embeddings and Search — kNN recall behind context Entities and Predicates — how seeds drive the walk Candidates and Confidence — confidence-based ranking ### The Entity Data Model (concepts) The Entity Data Model TL;DR Use this page when your data is strongly structured and a wrong type or extra value should be rejected , not silently stored. Outcome: typed, schema-checked records that still get full time-travel and graph walks — the same guarantees as facts, plus enforcement. The unit is a five-part tuple: (entity, attribute, value, tx, op) with a validity window and provenance. Each attribute declares a value type and a cardinality : one (functional) or many (set-valued). Durable agent runs are built on this model — Spec/Run/Step are schema'd entities. What it is Where the Facts surface uses free-form string predicates and untyped values, the entity data model asks you to declare a schema first, then write strongly-typed records against it. The smallest unit is a five-part tuple — a datom : Part Meaning entity Which entity this statement is about (a stable id). attribute Which declared attribute is being set (e.g. works_at , name ). value The typed value — see value types . tx The monotonic write-transaction number that produced this statement. op add (assert) or retract (close). Each datom also carries a validity window ( valid_from … valid_until ), a confidence score, and an optional source . Records are immutable: an update writes a new datom and closes the old one, so the full history stays queryable. Example: setting Sarah's employer writes (Sarah, works_at, →Acme, tx=42, add) ; changing it later writes a retract for the old value and an add for the new one in the same transaction. Declaring an attribute Every attribute is registered once with a schema before use. The schema fixes its value type, its cardinality, and whether values must be unique. WHY: declaring up front lets the engine reject a wrong-typed or surplus write at the boundary instead of corrupting the record. works_at — value type reference , cardinality one (one employer at a time). tag — value type string , cardinality many (many tags allowed). Value types Type Use for reference Graph edges — pointing from one entity to another. integer Counts, steps, versions. float Scores, percentages, measurements. string Labels, names, goals, content. boolean Flags. timestamp Instant values (distinct from the validity-window timestamps). Cardinality Cardinality is the schema-level equivalent of the conflict policy on the Facts surface — it just lives in the schema instead of being chosen per write: one — at most one active value per entity. Writing a new value closes the previous one with a retract and opens an add . Equivalent to a functional predicate / auto-supersede. many — new values are appended; old ones stay active until explicitly retracted. Equivalent to the coexist policy. Reading the store The read surface gives you, by behaviour: the current state of an entity — all its active attributes and values; time-travel — the same query as_of a past instant returns the state as it was then, including values since superseded; graph walks — follow reference values outbound or inbound to a chosen depth; an integrity check that surfaces orphans, dangling references, and contradictions. When to use entities vs. facts Use facts when you store heterogeneous claims about the world and do not need strict schema or typed graph traversal. Use schema'd entities when you have well-defined entity types, need type-safe attribute access, or are relying on durable agent runs (which are built on this model). Both share one managed storage layer — there is no duplication and no sync between them. Facts and Triples — the free-form counterpart Durable Agent Runs — built on the entity data model The Four Surfaces — where this model fits ### Embeddings and Search (concepts) Embeddings and Search TL;DR Every fact is embedded at assert time when an embedding service is configured. Default: no provider — embedding is off unless you set ABAGRAPH_EMBEDDING . Built-in providers: hash (deterministic, 64-dim, test-safe) and openai (real vectors via the OpenAI embeddings API or any compatible endpoint). Semantic search filters correctly on tenant, bitemporal validity, and status — no post-filter needed. How embedding works When a fact is asserted, the engine renders it to a short text representation (subject, predicate, object, source) and calls the configured embedding service to produce a fixed-dimension float vector. That vector is stored alongside the fact and indexed for kNN retrieval. If no provider is configured ( ABAGRAPH_EMBEDDING is unset), the embedding field is absent and semantic search returns empty results — no crash, just no answers. This is the correct default for development and offline use. Embedding providers Three providers are available, selected via the ABAGRAPH_EMBEDDING environment variable: Value Provider Dimensions (default) When to use unset None — Development, offline, or when you supply embeddings manually. hash Deterministic hash 64 (override with ABAGRAPH_EMBEDDING_DIM ) Tests, demos, reproducible runs. Not semantically meaningful. openai or http OpenAI-compatible HTTP 1536 (override with ABAGRAPH_EMBEDDING_DIM ) Production. Calls the OpenAI embeddings API or a compatible endpoint. Hash provider The hash provider maps each text to a fixed-dim vector using a byte-rolling hash, then L2-normalises. The algorithm is stable across restarts, so it is useful for reproducible tests. It is not semantically meaningful — "alice" and "employee" will not be close in hash space. Configure it on a dedicated deployment: ABAGRAPH_EMBEDDING=hash with an optional ABAGRAPH_EMBEDDING_DIM to set the vector dimension. OpenAI / HTTP provider The HTTP provider calls the OpenAI embeddings API (or any compatible endpoint). Required configuration for a dedicated deployment: Variable Description ABAGRAPH_EMBEDDING Set to openai or http . ABAGRAPH_EMBEDDING_API_KEY API key. Falls back to OPENAI_API_KEY if unset. ABAGRAPH_EMBEDDING_MODEL Model name, e.g. text-embedding-3-small . ABAGRAPH_EMBEDDING_DIM Vector dimension, e.g. 1536 . ABAGRAPH_EMBEDDING_BASE_URL API base URL (default: https://api.openai.com/v1 ). Set this to point at any compatible endpoint. Dimension mismatch. Once facts are stored with a given embedding dimension, changing ABAGRAPH_EMBEDDING_DIM will produce vectors that cannot be compared to existing ones. kNN results will be degraded or nonsensical. Set the dimension once and never change it on a persistent store. Semantic search The search endpoint embeds the question and performs a kNN scan over fact embeddings. The default top_k is 8 . The filter inside the scan respects tenant, bitemporal validity, and status. A fact is a candidate only if: Its tenant matches (or no tenant filter is set). For a live query: status == Active . For an as_of query: (Active or Superseded) and valid_from <= t < valid_until . These filters run inside the kNN scan — the engine never retrieves a global top-k and post-filters. This is important for correctness in multi-tenant deployments. REST API curl -X POST $ABAGRAPH_BASE_URL/api/search \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"question": "who does alice work for?", "top_k": 5}' Bulk ingest and embeddings On bulk ingest paths, the engine batches all embedding calls before persisting facts — embedding the whole payload in one round trip is more efficient than embedding per-assert. The POST /api/digest endpoint does this automatically. Context Packets — kNN recall inside context assembly Facts and Triples — the embedding field on Fact Tenancy and Isolation — tenant filter in kNN ### Entities and Predicates (concepts) Entities and Predicates TL;DR An entity is any string used as a subject or object — no registration required. The convention "Type:id" (e.g. "Person:alice" ) is strongly recommended but not enforced. A predicate is any string. Functional predicates (one value per subject) use AutoSupersede ; set-valued ones use Coexist . Text objects are walkable as graph nodes — the fact store is also a graph. EntityRef — identifying entities In abagraph, an entity is simply a string that appears as the subject or as a Text object of a fact. There is no entity registration step — an entity comes into existence the moment any fact references it. The API accepts any non-empty string as an entity identifier. The Type:id convention By convention (enforced nowhere, followed everywhere) entity identifiers use a "Type:id" format that encodes both what kind of thing the entity is and its unique identifier: "Person:alice" — a person named alice "Org:acme" — an organisation called acme "Document:report-q3-2024" — a document "Tenant:myproject" — the registry node for a project (used internally) The type prefix is a namespace that prevents collisions between, say, "File:readme" and "Article:readme" . Use PascalCase for the type and any format you prefer for the identifier portion. Predicates A predicate is any non-empty string describing the relationship between subject and object. Predicates fall into two semantic categories, determined by the ConflictPolicy used when asserting: Functional predicates (AutoSupersede) Most predicates are functional : at any moment, a subject has at most one active object for a given predicate. When you assert a new value, the old one is atomically closed (status becomes Superseded ). Examples: "works_at" — Alice works at exactly one organisation at a time. "status" — a document has one status. "confidence_score" — a rating has one numeric value. No configuration needed — AutoSupersede is the engine default. Set-valued predicates (Coexist) Some predicates allow multiple active values simultaneously. Pass ConflictPolicy::Coexist as policy_override on the FactInput for each assert. Examples: "tag" — a document can have many tags. "references" — a wiki page cites multiple sources. "cites" — a citation predicates multiple references. "mentions" — an entity can mention many others. # All three tags stay active simultaneously for tag in rust database agents; do curl -X POST $ABAGRAPH_BASE_URL/api/facts \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"subject\":\"Document:readme\",\"predicate\":\"tag\",\"object\":\"$tag\",\"policy_override\":\"coexist\"}" done Coexist is per-assert, not per-predicate. There is no global schema entry that marks a predicate as set-valued. You must pass ConflictPolicy::Coexist on every assert to a set-valued predicate. Forgetting it on one assert will supersede a previous value you meant to keep. Entities and edges — the graph When the object of a fact is a Text value, abagraph treats it as an entity reference. This turns the fact store into a directed graph: subjects and string objects are nodes, predicates are typed edges. The walk() method follows these edges with direction, depth, and edge-type filters: # Who does alice work for, and what does that org do? curl "$ABAGRAPH_BASE_URL/api/walk?entity=Person:alice&depth=2&direction=outbound" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Response includes reached entities and the edges traversed Getting an entity Fetch all active facts about an entity: curl "$ABAGRAPH_BASE_URL/api/entities/Person:alice" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Response: { "id": "Person:alice", "facts": [{ "predicate": "works_at", "object": "Org:acme" }, …] } Naming guidelines Use snake_case for predicates: "works_at" , not "worksAt" . Use stable, global identifiers in the id part: UUIDs, slugs, or natural keys — not row numbers. If the id is a URL, encode it: "Page:https%3A//example.com/about" or use a slug. The type prefix should be a stable noun: "Person" , "Org" , "Document" , "Task" . Facts and Triples — the full Fact struct Conflict and Supersession — AutoSupersede vs. Coexist Bitemporal Time — timestamps on facts ### Facts and Triples (concepts) Facts and Triples TL;DR A fact is { subject, predicate, object } plus provenance: two timestamps, confidence, source. The object is typed: Text , Number , Bool , Blob , or Null . Every fact carries a monotonic tx number so consumers can replay diffs with tx_gt . Asserting with FactInput auto-fills defaults; you only supply what you know. The Fact struct A Fact is immutable once written. Every field is set at assert time and never mutated in place — the only change that can happen to a fact is a status transition (Active → Superseded, or Active → Retracted). Field Type Description id FactId (UUID v4 string) Unique identifier; generated on assert. subject EntityRef (String) The entity this claim is about. Conventionally "Type:id" , e.g. "Person:alice" . predicate Predicate (String) The relationship name, e.g. "works_at" , "tag" , "confidence_score" . object FactObject The value. See FactObject variants below. valid_from i64 (ms since Unix epoch) Start of the validity window — when this claim became true in the world. valid_until Option<i64> End of the validity window. None means open-ended (currently true). recorded_at i64 (ms since Unix epoch) When the engine learned this fact — the write-side clock, always now(). confidence f64 (0.0–1.0) How sure are we? Facts below the engine's threshold land as Candidate . source Option<String> Who or what supplied this claim (URL, agent name, document id, …). properties Option<serde_json::Value> Arbitrary JSON metadata — why , how , reasoning traces, etc. status FactStatus Lifecycle state. See FactStatus below. tenant Option<String> Namespace for multi-tenant isolation. None is global/admin-owned. transient bool When true, the TTL sweeper hard-deletes the fact once valid_until passes. tx u64 Monotonic write-transaction number. All facts from one write share a tx. Replay with tx_gt . FactObject variants The object column is an untagged enum serialised as the native JSON type: Variant JSON shape Use for Text(String) "acme" Entity references, labels, IDs, any string value Number(f64) 42 , 3.14 Measurements, scores, counts Bool(bool) true Flags Blob { hash, size } {"hash":"<sha256>","size":1024} Content-addressed binary stored in the blob store Null null Explicit absence of a value A Text object is treated as a walkable entity reference — any string object can be a graph node that walk() will follow. FactStatus lifecycle Status Meaning Visible by default Active Currently valid; participates in conflict resolution. Yes Candidate Confidence below threshold; stored and searchable but does not supersede. Yes (filtered separately) Superseded Closed by a newer conflicting fact. Query-invisible unless you use as_of . Only via as_of Retracted Explicitly retracted. Hidden from queries by default. No Asserting a fact Every field has a default so you only supply what you know. The minimal request is just the triple; add provenance fields as needed: # Minimal: just the triple curl -X POST $ABAGRAPH_BASE_URL/api/facts \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:alice","predicate":"works_at","object":"Company:acme"}' # With provenance curl -X POST $ABAGRAPH_BASE_URL/api/facts \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subject": "Person:alice", "predicate": "works_at", "object": "Company:acme", "confidence": 0.95, "source": "onboarding-form" }' # Numeric value curl -X POST $ABAGRAPH_BASE_URL/api/facts \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:alice","predicate":"age","object":31}' Transient facts Setting transient: true combined with a valid_until gives Redis-style TTL: the TTL sweeper hard-deletes the fact when valid_until < now() . Non-transient facts keep their Superseded history forever — that is the default and the safe choice for agent memory. The tx cursor Every committed write (assert, retract, transact) allocates one monotonic tx and stamps it on every fact the write touches, including the superseded fact that was closed. Consumers that want a live feed of changes should note the tx from the last fact they processed, then request GET /api/facts?tx_gt=<last_seen> to replay only what is new. Bitemporal Time — valid_from/valid_until vs. recorded_at Conflict and Supersession — how contradictions are handled Candidates and Confidence — threshold and promotion Entities and Predicates — EntityRef conventions Guide: Assert a Fact — step-by-step walkthrough ### Mental Model (concepts) Mental Model TL;DR Everything is a fact : subject — predicate → object , plus time, confidence, source. Two timelines: valid time (true in the world) vs record time (when we learned it). Nothing is deleted. A contradiction supersedes the old fact — atomically, in one batch. as_of(t) is like git checkout for beliefs: the whole world as it was known at t . Facts are triples with a memory A fact is a claim: alice works_at Acme . Every fact carries five extras: Field Question it answers valid_from / valid_until When was this true in the world ? recorded_at When did we learn it? confidence How sure are we? (0.0 – 1.0) source Who/what told us? Two timelines, not one This is the trick that makes time-travel honest. Suppose Alice changed jobs in January, but your agent only found out in March. Valid time says the old job ended in January. Record time says you believed the old job until March. So as_of(February) truthfully returns the wrong-but-honestly-held belief — exactly what you need when auditing why an agent did what it did in February. Supersede, never delete Watch what happens on a contradiction: assert("alice", "works_at", "Acme") // fact A: active assert("alice", "works_at", "Beta") // fact A → Superseded (closed), // fact B: active — one atomic batch Fact A is still there, queryable through history. Most predicates are functional like this — one active object per subject. Set-valued predicates ( references , tag , …) opt into coexistence per assert, so multiple objects stay active side by side. Candidate facts. Assert below the confidence threshold and the fact lands as a Candidate : stored, searchable, but it does not supersede anything. Reassert at full confidence to promote it. Low-quality input can't silently overwrite good memory. Entities and edges Subjects and objects are entities ; predicates are typed edges between them. That makes the store a graph: walk() follows edges with direction, type, and time filters. Prose can join in too — the wiki layer turns [[Entity:foo]] links in text into real references edges. Common mistakes Confusing the two timelines. Valid time = the world; record time = your knowledge of it. Expecting DELETE . Retraction closes a fact's validity; purge (admin-only) is the single true-delete escape hatch. Forgetting conflicts are tenant-local : a fact in tenant A never supersedes a fact in tenant B. ### What is abagraph? (concepts) What is abagraph? TL;DR abagraph is a managed memory database for AI agents : a bitemporal graph + vector store in one engine, reached over a REST API and an MCP server. Facts are never deleted — they are superseded , so you can time-travel to any past belief. Four surfaces, one storage layer: Facts (REST) , Entities (schema'd) , Durable agent runs , MCP "orchestrate" . The elevator pitch Agents forget. They lose context between runs, cannot say where a "fact" came from, and cannot answer "what did I believe last Tuesday when I made that call?" abagraph fixes that. It stores knowledge as subject–predicate–object triples — Person:alice works_at Org:acme — each stamped with when it was true, when we learned it, how confident we are, and where it came from. Contradictions do not overwrite history; they close the old fact and open a new one. On top of that: semantic search (every fact gets an embedding), graph walks (follow typed edges), and context packets (one call returns everything an agent should know before a task). The four surfaces Everything below sits on one storage layer. Pick the surface that matches your job: Surface Use it when… Reach it via Facts (REST) You want to assert and recall free-form claims with provenance and time-travel. /api/facts , /api/search , /api/context Entities (schema'd) You need schema-checked records — typed attributes with declared cardinality. Attribute schema + entity endpoints Durable agent runs You are running a resumable agent loop (spec → run → steps) that must survive a crash. Run and step endpoints (preview) MCP "orchestrate" An LLM should use memory as tools: remember , recall , build_context … stdio MCP or POST /mcp Reading path New here? Read Mental Model first — everything builds on it. Then Getting Started . Choosing a surface? The Four Surfaces has a decision table. Understanding behaviour? Architecture explains the service surfaces and guarantees; Conflict and Supersession explains atomic writes. Wondering why this exists? North Star & Vision . Lost in a term? Glossary . Common mistakes Treating abagraph like a CRUD database. There is no update-in-place — see Mental Model . Reaching for schema'd entities when the Facts API is enough. Free-form claims → facts; schema-enforced records → entities. ### The Four Surfaces (concepts) The Four Surfaces TL;DR Use this page when you are choosing how to talk to abagraph for a given job. Facts (REST) — store and query free-form claims with provenance and time-travel. Entities (schema'd) — typed, schema-checked records when you need cardinality and value-type guarantees. Durable agent runs — an agent loop where every step is saved atomically, so a crashed run resumes from where it stopped. MCP "orchestrate" — memory exposed to any LLM as seven outcome-named tools. All four read and write the same managed store — no copies, no sync. I want to… → use this surface I want to… Surface Store a claim like "Sarah works at Acme" with a source and confidence Facts (REST) Ask "what do I know about Sarah?" or "what did I believe last Tuesday?" Facts (REST) Keep typed records where each attribute has a declared type and cardinality Entities (schema'd) Run a long agent task that must survive a crash and resume Durable agent runs Give an off-the-shelf LLM memory tools it can call directly MCP "orchestrate" Facts (REST) — claims you can audit The everyday surface. A fact is a subject–predicate–object triple plus provenance: Person:sarah --works_at--> Org:acme with valid_from , recorded_at , confidence , and source . You assert triples, query by pattern, walk edges, search semantically, and time-travel with an as_of timestamp — all over HTTP. Conflict handling is automatic: asserting a contradicting value closes the old fact and opens the new one in a single atomic write, so nothing is ever silently lost. See API Reference: /api/facts . Entities (schema'd) — records with declared types When your data is strongly structured, register a schema first: each attribute declares a value type (string, number, reference, boolean, timestamp) and a cardinality — one (at most one active value, like works_at ) or many (set-valued, like tag ). Writes are then type-checked, and reads support the same time-travel and graph walks as facts. Reach for this when a value type or cardinality constraint must be enforced rather than assumed. See the entity data model . Durable agent runs — loops that survive crashes The problem: agent loops die mid-run and lose everything. The answer: a run is made of steps , and every step is committed atomically before the loop continues. Kill the process, ask to resume the same run, and it picks up from the last saved step — no state lost. A budget (max steps / max tokens) and an optional verifier decide when a run is done; a run can also park for human approval and resume later. See Durable Agent Runs . MCP "orchestrate" — memory as tools Seven outcome-oriented tools — named for what the caller wants, not for any internal mechanism — let any MCP-capable LLM use abagraph as memory: Tool Use it when… digest Turning raw text/data into facts. remember Storing one claim, with provenance. recall "What do I know about X?" recall_as_of "What did I believe at time T?" whats_changed "What is new since T?" build_context "Brief me before this task." explain_fact "Where did this claim come from?" The MCP surface is reachable two ways: a stdio MCP connection for local LLM hosts, and POST /mcp on the hosted API (same auth as REST). Every tool returns a bounded, provenance-rich projection ( fact_view ) — id, subject, predicate, object, confidence, source, status, and validity — and never raw embedding vectors. The namespace argument is your tenant: reads filter on it and conflict resolution stays inside it, so two agents on different namespaces never collide. Common mistakes Reaching for schema'd entities when free-form facts would do. Start with Facts; graduate to schema'd entities only when a type or cardinality constraint must be enforced. Treating recall as a database scan. It returns the most relevant bounded set, ranked by confidence and similarity — ask a sharper question, not for "everything". Passing a different namespace to escape your tenant. A scoped caller is hard-isolated to its own namespace regardless of what it passes. Facts and Triples The entity data model Durable Agent Runs Context Packets ### Tenancy and Isolation (concepts) Tenancy and Isolation TL;DR Every fact carries an optional tenant field. Reads filter on it; writes stamp it. API keys are provisioned by the managed service: a per-tenant key is scoped to one namespace; an admin key spans all namespaces. Subdomain routing maps nas.abagraph.com to tenant "nas" automatically. The tenant field Every Fact has an Option<String> tenant . The managed service stamps this field from the authenticated identity before writing. Callers cannot override it — a tenant-scoped token always writes to its own namespace, never another's. None tenant is the global/admin scope. Admin-owned facts ( tenant = None ) are readable by admin requests but not by tenant-scoped ones. Authentication The managed service provisions two kinds of API keys: Admin key An admin key grants access to all tenants and all admin endpoints ( /api/tenants , /api/admin/* ). Use it for operational tasks, not for application code. Per-tenant key A per-tenant key is scoped to exactly one namespace. A request bearing that key can only read and write facts in its own namespace. Using a tenant key on a different tenant's subdomain is rejected — the key is valid but it is presented to the wrong tenant. Cookie session auth The server also accepts a session cookie as an alternative to the Authorization header. Establish a session via: curl -X POST $ABAGRAPH_BASE_URL/api/session \ -H "Content-Type: application/json" \ -d '{"key": "<your-token>"}' # Sets: Set-Cookie: abagraph_session=...; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000 The cookie is HttpOnly, Secure, SameSite=Lax, and expires in 30 days (2592000 seconds). Delete it via DELETE /api/session . Subdomain routing The managed service extracts the subdomain as the tenant. A request to nas.abagraph.com is automatically scoped to tenant "nas" . A key presented on the wrong subdomain is rejected even if the key itself is valid for another tenant — the subdomain and the key's tenant must match. The single isolation chokepoint All reads pass through a single filter that adds the tenant scope before any query runs. This is the architectural chokepoint: there is one place to audit, one place to test. Branch reads are the only exception — they reconcile the overlay explicitly before calling into the query layer. Conflict resolution is tenant-local When the engine checks for conflicts before asserting a fact, it filters existing active facts by fact.tenant == new_fact.tenant . A fact in tenant "acme" can never supersede a fact in tenant "beta" — even on a shared subject and predicate. This is enforced in the engine's conflict detection layer and is not configurable. Tenant ID format Tenant IDs (created via POST /api/tenants ) must be: Non-empty and at most 64 characters. ASCII alphanumeric characters and hyphens ( - ) only. No underscores, no ~ (reserved for branch derived namespaces), no : . Examples: acme , my-project-1 . Invalid: under_score , has~tilde , has:colon . Public endpoints These endpoints are accessible without any auth token: GET /api/health POST /api/signup Static UI assets and the docs shell /llms.txt , /llms-full.txt , /openapi.json , /sitemap.xml All /api/* endpoints (other than /api/health and /api/signup ) require authentication. MCP namespaces The MCP surface uses namespace as the tenant argument. A tenant-scoped caller (HTTP POST /mcp with a tenant token) is pinned to its namespace regardless of the namespace argument it passes. Only admin callers (the stdio binary or an admin-token HTTP caller) can switch namespaces via the argument. Conflict and Supersession — tenant-local conflict scope Branches — derived tenant namespace for overlay writes Facts and Triples — the tenant field on Fact ### Durable Agent Runs (concepts) Durable Agent Runs TL;DR Use this page when an agent task is long enough that a crash, timeout, or redeploy mid-run would be expensive. Outcome: a run where every step is one atomic save — kill it, resume it, and it continues from the last saved step with no state lost. Three record types: Spec (what to do), Run (one invocation), Step (one turn). A budget caps steps and tokens; an optional verifier decides success; a tool can park the run for human approval . Durable runs are preview — not yet served through the public API. Track it on the durable-workflows roadmap . The problem this solves An agent loop held only in process memory loses everything on any crash, timeout, or deployment. Durable runs make the agent's own history the source of truth: the Spec, the Run, and every Step are persisted records, and each step is committed atomically before the loop takes another turn. If the process dies mid-run, resuming the same run continues from the last committed step. Spec, Run, and Step Record What it is Key fields Spec What the agent should accomplish; reused across many runs. role, goal, model, autonomy policy, tool scope, success criteria Run One durable invocation of a Spec. spec, trigger, status, budget (max steps, max tokens) Step One turn within a run. run, index, kind, content, tokens in/out Example: a Spec with goal="find all mentions of project Atlas" and success_criteria="atlas" can back many runs; each run records its steps and its final outcome independently. What happens each turn Check the budget. If the step or token budget is exhausted, the run closes with outcome Budget and parks (it can be resumed). WHY: budgets bound cost without throwing away progress. Assemble context. The engine builds a per-turn context packet: recent steps verbatim, older steps compressed into a summary, semantically recalled facts, an entity graph walk, and relevant wiki pages. Call the model. The model returns one of: a Message , a ToolCall , or Done . Persist the step. One atomic save. Token counts update the budget. Mirror to memory. Step content is also written into the fact store for future semantic recall. This is best-effort — a mirror failure never fails the step. Act on the result. Message : save an assistant step, continue. ToolCall : save the call, run the tool, save the result. tool requests approval: save an awaiting step, set status to awaiting-approval, return to the caller for a human gate. Done : save the final step, run the optional verifier, close as success or failure. Budget The budget tracks turns , not on-disk steps: a tool-call turn writes two steps (the call and the result) but counts as one turn. Two limits come from the run: max steps — maximum turns before the run closes as Budget . max tokens — maximum cumulative tokens (in + out) before closure. Either limit, once reached, triggers closure on the next loop iteration — not mid-step — so the last committed step is always complete. WHY: a resumed run never sees a half-written turn. Run outcomes Outcome Meaning Resumable? Success Model finished and the verifier approved. Terminal Failure Model finished but the verifier rejected the result. Terminal Budget Step or token budget exhausted. Yes — parked, resume to continue AwaitingApproval A tool requested human approval. Yes — parked, resume after approval Budget and AwaitingApproval park the run rather than ending it. Resuming the same run replays from the last step index. Verifier The optional verifier is invoked when the model returns Done . If it rejects the output the run closes as Failure . A simple built-in verifier checks that every meaningful token of the Spec's success_criteria appears in the final output — useful as a baseline before you supply a stricter one. The entity data model — the typed records durable runs are built on Context Packets — what is assembled each turn The Four Surfaces — how durable runs fit alongside the other surfaces Roadmap: durable workflows — availability through the public API ### The Four Surfaces (Detailed) (concepts) The Four Surfaces (Detailed) TL;DR Use this page when you know which surface you want and need its behaviour, params, and limits. Facts (REST) — free-form triples with provenance: assert → query → as_of → walk → search . Entities (schema'd) — typed records with declared value-type and cardinality constraints. Durable agent runs — loops where every step is one atomic commit, resumable after a crash. MCP "orchestrate" — seven intent-named tools over a stdio connection or POST /mcp . All four read and write one managed store. Pick the surface that fits the job, not your preference. Facts (REST) The everyday surface and the one most application and agent code uses. It does not require a schema or agent-loop durability — just assert claims and read them back. Key operations Operation What it does POST /api/facts Assert one fact. Handles conflict resolution and embedding automatically. DELETE /api/facts/:id Retract a fact — closes its validity window; history is preserved, not hard-deleted. GET /api/facts?subject=…&predicate=… Pattern-match over active facts. GET /api/walk Walk the graph from an entity with direction, depth, and edge-type filters. POST /api/search Semantic (k-nearest-neighbour) search over fact embeddings. GET /api/facts?as_of=… Time-travel: a snapshot of the world at that instant, including since-superseded facts. POST /api/context One-call context packet: ranked facts + graph walk + wiki pages. POST /api/transact Compare-and-swap atomic multi-fact write. A minimal assert over HTTP: curl -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme","confidence":0.95,"source":"onboarding-form"}' \ 'https://acme.abagraph.com/api/facts' Conflict handling By default, asserting a new value for a functional predicate (one active object per subject, e.g. works_at ) atomically closes the existing active fact and opens the new one. Set-valued predicates like references and tag use a coexist policy per assert, so multiple values stay active. WHY: an employer change should replace the old one, but a second tag should not erase the first. Entities (schema'd) When data is strongly structured, declare a schema first and the engine type-checks every write. Each attribute has: a value type — string, 64-bit integer, float, entity reference, boolean, or timestamp; a cardinality — one (at most one active value per entity) or many (set-valued). Reads support the same time-travel snapshots and graph walks as facts, plus an integrity check that surfaces orphans, dangling references, and contradictions. Cardinality one behaves like the functional-predicate / auto-supersede rule on the Facts surface; cardinality many behaves like the coexist rule — the constraint just lives in the schema instead of being chosen per write. When to reach for schema'd entities vs. facts. Facts suit free-form claims where the subject–predicate–object model is natural and no schema is needed. Schema'd entities suit structured records where attribute cardinality and type constraints must be enforced — for example the Spec/Run/Step records behind durable agent runs. Durable agent runs This surface solves one problem: agent loops that must survive process restarts. The key idea is that a run's history is the source of truth, not in-memory state: A Spec describes what the agent should do (role, goal, model, autonomy policy, tool scope, success criteria) and is reused across many runs. A Run is one invocation of a Spec, with a budget (max steps, max tokens) and a trigger. A Step is one turn — an assistant message, a tool call, or a tool result — committed as one atomic write before the loop continues. Each turn the engine assembles a context packet (recent steps verbatim, older steps compressed, semantic recall, graph walk, wiki pages), calls the model, persists the step, and mirrors the step content into the fact store for future recall. If the process dies, resuming the same run replays from the last saved step. A budget governor and an optional verifier close the run; a tool can request human approval, which parks the run until it is resumed. Durable agent runs are preview and are not yet served directly through the public API. Track availability on the durable-workflows roadmap . Run outcomes Outcome Meaning Success Model finished and the verifier approved. Failure Model finished but the verifier rejected the result. Budget Step or token budget exhausted. Run is parked, not dead — resumable. AwaitingApproval A tool requested human approval. Run is parked — resumable. MCP "orchestrate" The MCP surface exposes memory to any MCP-capable LLM as seven JSON-RPC tools named by intent. It is reachable two ways with identical behaviour: a stdio MCP connection for local LLM hosts, and POST /mcp on the hosted API, gated by the same Bearer-token / cookie auth as REST. The seven tools Tool Use it when you want to… digest Load a batch of pre-extracted facts into a namespace. remember Store one claim with provenance. recall Query by semantic search, pattern, or graph walk. recall_as_of See what the namespace believed at a past timestamp. whats_changed Get added, superseded, and still-valid facts between two timestamps. build_context Get a ranked context packet before a task. explain_fact Fetch the provenance history for one fact. Every tool response is a fact_view projection — provenance-rich (id, subject, predicate, object, confidence, source, status, validity) but never raw embedding vectors. The namespace argument is the tenant; a scoped caller is hard-isolated to its own namespace even if it passes a different one. Tool profiles The visible tool list can be restricted to a profile: agent-memory — build_context , digest , remember , recall , whats_changed read-only — recall , recall_as_of , explain_fact full — all seven tools Facts and Triples The entity data model Durable Agent Runs Context Packets ### North Star & Vision (concepts) North Star & Vision TL;DR Use this page when you need the one-paragraph "why abagraph exists" for a pitch, a PRD, or a teammate. abagraph is a proprietary, managed database for the AI era — the system-of-record for agent memory. Every era of computing gets a system-of-record: analytics got the warehouse, then the lakehouse. The agent era needs one for memory — that is the AIhouse bet. Winning = agents that trust their own memory , because every claim carries a source, a confidence score, and two timestamps. One engine instead of three: no vector DB + graph DB + audit log duct-taped together. The problem Today's agents are brilliant and amnesiac. They forget between sessions, they hallucinate provenance ("where did I learn this?" — no answer), and when a decision goes wrong nobody can reconstruct what the agent believed at the moment it decided . Teams patch around it: a vector store for recall, a graph database for relationships, application logs for audit. Three systems, three consistency models, zero shared notion of time or truth. The bet Memory is not a feature you bolt on — it is a system-of-record , and it needs three properties in one engine: Bitemporal — two timelines per fact: valid time (when it was true in the world) and record time (when we learned it). WHY: history becomes queryable data, never garbage to be overwritten. Graph-native — knowledge is entities and typed edges, so agents can walk relationships, not just match keywords. WHY: "who reports to Sarah's manager?" is a graph walk, not three keyword searches. Vector-ready — every fact is embedded, so "what do I know about X?" works even when the words do not match. WHY: agents ask in natural language, not exact strings. A concrete unit of memory: Person:sarah --works_at--> Org:acme , stamped valid_from=2026-01-01 , recorded_at=2026-01-03 , confidence=0.95 , source="onboarding-form" . One row answers "is this true now?", "was it true last Tuesday?", and "who told us?". The analogy we steer by: warehouse → lakehouse → AIhouse . The warehouse was where the business's numbers became trustworthy. The lakehouse unified structured and unstructured analytics. The AIhouse is where an agent's beliefs become trustworthy. A proprietary, managed product abagraph is a proprietary, hosted service , not a library you operate yourself. You reach it through a documented REST API and an MCP server ; a managed console gives humans a window into the same data. You never run the storage engine, manage replicas, or tune indexes — that is our job. Select components may become source-available later , once the product is fully ready; that is on the roadmap, not available today. What winning looks like An agent answers a question and can show its work: source, confidence, and validity window — for every claim. A developer debugging an agent runs a time-travel query ( as_of(incident_time) ) and sees exactly the world the agent saw — with no separate audit log to reconcile. A platform team retires three storage systems and points everything at one managed memory API. Non-goals (read before proposing features) These are deliberate. Features that fight them will be turned down: Not a general-purpose OLTP database. abagraph stores beliefs, not shopping carts. No in-place update. There is no destructive UPDATE — a contradiction opens a new fact and closes the old one. WHY: time-travel, audit, and branching all depend on history being immutable. Agent-first, human-second. The primary customer is an agent in a loop; the human console is the second surface, not the first. Common mistakes Pitching abagraph as "a vector store with extras". The provenance + time model is the product; vectors are one index over it. Designing features for human dashboards first. The primary consumer is an agent calling the API; humans get the console second. ### What is abagraph? (Data Model Deep Dive) (concepts) What is abagraph? (Data Model Deep Dive) TL;DR Use this page when you need the full picture of the data model before building on abagraph. abagraph is a proprietary, managed database for the AI era — the system-of-record for agent memory. The mechanism: a bitemporal graph + vector store in one engine , reached through a REST API and an MCP server. Every piece of knowledge is a fact : a subject–predicate–object triple stamped with two timestamps, confidence, and a source. Facts are never overwritten . A contradiction closes the old fact and opens a new one atomically — that is what makes time-travel and audit free. The problem abagraph solves AI agents are amnesiac by default. Between sessions they forget what they learned, they cannot trace a claim back to its source, and no tool can reconstruct what the agent believed at the moment a decision was made. Teams patch around this with three separate systems: a vector store for recall, a graph database for relationships, and application logs for audit. Three consistency models, three operational surfaces, no shared notion of time or provenance. abagraph collapses those three into one managed engine: a bitemporal graph + vector memory store for LLM agents, wiki-native. What abagraph is At its core, abagraph is a fact store . Every claim it holds is a subject–predicate–object triple augmented with: Two timestamps: when the claim was true in the world ( valid_from / valid_until ) and when you learned it ( recorded_at ). This is bitemporality . A confidence score (0.0–1.0). An optional source string (who or what told you). An embedding vector, generated automatically, so semantic search works out of the box. A concrete fact: Person:sarah --works_at--> Org:acme , valid_from=2026-01-01 , recorded_at=2026-01-03 , confidence=0.95 , source="onboarding-form" . On top of that fact store sit four read/write surfaces — each for a different job — all sharing one storage layer. There is no duplication of data between surfaces. The four surfaces at a glance Surface Reach it via Best for Facts (REST) /api/facts , /api/search , /api/context Free-form claims with provenance and time-travel Entities (schema'd) typed records with declared schemas Structured data that must be type-checked Durable agent runs (preview) Spec → Run → Steps Long agent tasks that must survive a crash MCP "orchestrate" stdio MCP or POST /mcp Memory exposed to an LLM as seven tools Each surface is described in full on The Four Surfaces . A managed, proprietary service abagraph is a hosted product , not software you operate. You reach it over the documented REST and MCP APIs; a managed console gives humans a window into the same data. You never run the storage engine, manage replicas, or tune indexes. Select components may become source-available later , once the product is fully ready — that is on the roadmap, not available today. Design intent Several choices are deliberate and will not change. Knowing them up front prevents misplaced expectations: No in-place update. There is no destructive UPDATE. A contradiction produces a new fact and closes the old one. WHY: this single invariant is what makes time-travel, audit, and branches free — everything else depends on it. Provenance is mandatory, not optional. Every fact carries source, confidence, and two timestamps. WHY: an agent can only trust memory it can interrogate. Not a general-purpose OLTP database. abagraph stores beliefs about the world. It is not for shopping carts, financial ledgers, or relational join workloads. Agent-first. The primary consumer is an agent in a loop calling the API; the human console is the second surface. Facts and Triples — field-by-field breakdown of a fact Bitemporal Time — valid time vs. record time explained The Four Surfaces — detailed surface comparison Mental Model — the brief conceptual intro Getting Started — your first calls in five minutes ### aba — the terminal UI (getting-started) aba — the terminal UI TL;DR aba is Claude Code, but for your memory database — an interactive terminal UI for abagraph. Run it with zero install: bunx @abagraph/tui (Bun required). It is published on npm as @abagraph/tui . It is a pure client : speaks only to your instance's REST API and /mcp endpoint. It ships no engine code and collects no model API keys — answers are synthesized by an agent CLI you already have ( claude , codex ) running locally , with tools disabled. Reuses the same ~/.abagraph.json the abagraph CLI writes — npx abagraph init … then aba just works. 1. Install The quickest path needs only Bun — no install step at all: # zero-install, always the latest published version bunx @abagraph/tui Prefer it on your PATH like Claude Code? Install it globally: bun add -g @abagraph/tui # then just: aba The package bundles an install.sh (bootstraps Bun if needed), and tagged releases publish a compiled, standalone aba binary per platform — no Bun required to run it. 2. Connect aba reads connection details ( url + key ) from ~/.abagraph.json — the same file the abagraph CLI writes — so the two tools interoperate. Three ways to provide them, in precedence order: # a) shared config file (recommended — written by the CLI) npx abagraph init --url https://nas.abagraph.com --key <your-token> aba # b) environment ABAGRAPH_URL=https://nas.abagraph.com ABAGRAPH_KEY=<your-token> aba # c) first run with no config opens an interactive connect wizard Your key is a secret. aba writes ~/.abagraph.json with 0600 permissions. Prefer the config file or env over the --key flag (flags are visible in ps and shell history). 3. Load the sample and ask With no data yet, load the built-in demo — Bob (a BDR) ran a discovery call with Google , who reported that data compression loses facts ; R&D is now root-causing it: /sample # load the demo dataset /onboard # show the getting-started card Then just type a question. aba gathers evidence from your graph and your local agent writes a cited answer with a measured reliability score: what did Google report on the discovery call? why is R&D involved? No local agent installed? aba falls back to an evidence-only answer (the cited facts, no synthesis). Choose or disable the agent any time with /model claude|codex|off|cmd "<command>" . 4. Explore the graph Recall renders as a branded table with trust signals (status glyph, confidence bar, provenance): /recall Person:bob # pattern query → table /recall --predicate reported --status active /search "compression problems" # semantic kNN over facts /walk Org:google --depth 2 --dir both # graph traversal /graph Org:google # interactive view: ↑/↓ move, Enter to walk, Esc to close 5. Digest raw data into facts Turn unstructured text into bitemporal facts. Extraction runs on your local agent; --commit writes them: /digest "Acme signed a 2-year contract on 2026-06-01; champion is Dana Lee, VP Eng." /digest "…" --commit # write the extracted triples /digest --facts "Org:acme|signed|Contract:2yr; Person:dana|title|VP Eng" # commit pre-extracted triples 6. Pull a context packet — with control The payoff for an agent builder: assemble a bounded, provenance-rich context packet for a task, with live knobs over budget, recency, and framing. /context "brief me on the Google account" --budget 1500 --max-facts 20 --role analyst --style brief 7. Time-travel, branches & trust abagraph is bitemporal — every read can be re-scoped to a past instant, and speculative writes live on branches ( git for memory ): /asof -7d # re-scope all reads to one week ago /changed 2026-06-01 2026-06-29 # diff memory between two instants /branch create what-if # speculative writes off main /explain <fact_id> # full provenance + lifecycle of a fact /factcheck Org:google # surface contradictions / competing hypotheses How aba keeps you safe No keys collected. The model call happens via your local claude / codex auth. aba never sees a model API key, and tests assert the bearer token never reaches the agent prompt. Synthesis runs tools-disabled. The local agent is launched read-only ( claude --allowedTools "" ; codex --sandbox read-only ) and the grounding prompt marks evidence facts as untrusted data, never instructions — a prompt-injection payload stored as a fact cannot drive code execution on your machine. Tenant-scoped. REST reads send no namespace — your tenant is derived from your key server-side and cannot be widened. Where to next A visual, click-through walkthrough lives at /docs/onboarding.html . Type /help in aba for the full command palette, or /connect for your REST/MCP endpoints and snippets. Building an agent against the API directly? See Quickstart (REST) and Your first context packet . ### Authentication (getting-started) Authentication TL;DR The managed service always authenticates. Every protected endpoint requires a valid bearer token or session cookie; a missing or invalid key returns HTTP 404. Two credential types: bearer token (API calls) and cookie session (browser console). Subdomain routing maps your tenant subdomain (e.g. nas.abagraph.com ) to tenant nas . How auth works The managed service always authenticates every request. A missing or invalid key returns HTTP 404 before any data is read or written. When your tenant is provisioned you receive a per-tenant API key. Send it on every API call as Authorization: Bearer <your-key> . Your key and base URL are shown in the abagraph console under Connect . Bearer tokens Per-tenant token Your issued key is scoped to your tenant. Every read and write is filtered to that tenant — a key cannot access another tenant's data. # Replace <your-token> with the key shown in the console under Connect curl -H "Authorization: Bearer <your-token>" \ https://<tenant>.abagraph.com/api/facts One key per tenant. Each issued key is scoped to exactly one tenant. Tenant isolation is enforced by the service — all reads and writes are filtered to the caller's tenant and cannot be widened. SDK import { createClient } from '@abagraph/client' const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, // sent as: Authorization: Bearer <apiKey> }) CLI abagraph init --url https://gateway-host --key <your-token> # or per-call abagraph --key <your-token> query --subject Person:alice Cookie session The browser console authenticates with an HttpOnly cookie rather than a visible token. POST /api/session validates the key and sets the abagraph_session cookie. curl -s -c cookies.txt -X POST https://your-abagraph-host/api/session \ -H "Content-Type: application/json" \ -d '{"key": "<your-token>"}' {"scope": "admin"} The cookie is set with HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=2592000 (30 days). Use the cookie jar on subsequent requests: curl -s -b cookies.txt https://your-abagraph-host/api/facts To log out, DELETE /api/session : curl -s -b cookies.txt -X DELETE https://your-abagraph-host/api/session scope in the response is one of: "admin" — the key has admin scope (spans all tenants). "tenant" (with "tenant": "..." ) — the key is scoped to a single tenant. Subdomain-to-tenant routing The managed service routes subdomains to tenants automatically. A request to nas.abagraph.com is scoped to tenant nas ; your bearer token is still validated and must match that tenant. Subdomain extraction rules: Single-label subdomains only: nas.abagraph.com works; a.b.abagraph.com does not. www is excluded — the apex and www subdomain resolve to no tenant. Only ASCII alphanumeric characters and hyphens are accepted in the subdomain label. Port suffixes are stripped before matching ( nas.abagraph.com:443 → nas ). Subdomain routing is additive, not a replacement for token auth. A caller still provides their own bearer token or session cookie. The subdomain only determines the tenant scope — the token is still validated, and a token with the wrong tenant scope is rejected. Public endpoints The following endpoints never require a token, even when auth is enabled: GET /api/health POST /api/signup Static UI assets ( /console , /docs , etc.) GET /llms.txt , /llms-full.txt , /index.md , /openapi.json , /sitemap.xml MCP endpoint auth POST /mcp is auth-gated identically to the REST API. Include the same bearer token in the Authorization header. { "mcpServers": { "abagraph": { "url": "https://gateway-host/mcp", "headers": { "Authorization": "Bearer <your-token>" } } } } Production notes Store your API key in your secret manager — never in source code. Each issued key is scoped to one tenant. Use the key that matches your tenant to limit blast radius. To rotate a key, contact support or use the abagraph console; the old key is immediately invalidated on rotation. Next steps Quickstart: REST API — first curl calls Install the TypeScript SDK — how apiKey maps to the bearer header Install the CLI — configure the CLI with --key or ABAGRAPH_KEY Your First Context Packet — tenant-scoped context assembly ### Welcome to abagraph Docs (getting-started) abagraph documentation In one sentence — abagraph is a proprietary, managed database for the AI era : the system-of-record for agent memory, combining a bitemporal graph and a vector store in one engine — vector search, graph walks, time-travel queries, and durable agent loops, reached through a REST API and a seven-tool MCP server. Hosted service; early access. Pick your path You are… Start here Giving an LLM persistent memory via MCP Agent Integration Overview Calling the REST API from any language Quickstart: REST API Using the TypeScript / Node SDK Install the SDK Using the CLI to assert facts and manage branches Install the CLI Getting an API key and onboarding to the hosted service Authentication Learning the data model and concepts What is abagraph? Three calls to first result abagraph is a hosted service — you call it over HTTPS with your API key. No engine to install or run. # 1. check you can reach the service with your key curl -H "Authorization: Bearer $ABAGRAPH_TOKEN" \ https://acme.abagraph.com/api/health # 2. assert a fact with provenance and confidence curl -X POST https://acme.abagraph.com/api/facts \ -H "Authorization: Bearer $ABAGRAPH_TOKEN" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme","confidence":0.9,"source":"hr"}' # 3. get the context packet an agent reads before a task curl -X POST https://acme.abagraph.com/api/context \ -H "Authorization: Bearer $ABAGRAPH_TOKEN" \ -H "Content-Type: application/json" \ -d '{"seeds":["Person:sarah"],"goal":"brief me before 1:1","token_budget":4096}' Connect an LLM in 60 seconds (MCP) Point any MCP-capable LLM host at the hosted MCP endpoint. It speaks the same protocol as REST and uses the same Bearer token: { "mcpServers": { "abagraph": { "url": "https://acme.abagraph.com/mcp", "headers": { "Authorization": "Bearer ${ABAGRAPH_TOKEN}" } } } } Seven tools are then available: digest , remember , recall , recall_as_of , whats_changed , build_context , explain_fact . See MCP Setup and Tools Reference . What is here Section What you will find Getting Started REST quickstart, SDK and CLI install, authentication, first context packet Concepts Bitemporal time, facts and triples, conflict/supersession, branches, embeddings, durable runs Guides How-to walkthroughs: assert, transact, time-travel, digest, connect, branch, fact-check API Reference Every REST endpoint with request/response shapes and ?explain=1 query plans SDK TypeScript client, createAgentMemory wrapper, plugin and hook system CLI Command-line tool for asserting facts, querying, branches, and digest Agents MCP setup, seven tools reference, per-turn memory loop, tool profiles Graph Graph model, BFS walk API, wiki-link citations Security Auth model, tenant isolation, PII masking, BYOK model keys Configuration API options, query parameters, embedding providers Examples REST recipes, wiki graph, digest, agent run, CSV ingest recipe Errors All error codes with causes and remediation steps Glossary Bitemporal, AIhouse, supersede, context packet, datom, durable run… Core concepts (30-second version) Facts are subject–predicate–object triples stamped with valid time, record time, confidence, and source. They are never deleted — contradictions open a new fact and close the old one ( supersession ). Bitemporal time — two clocks per fact: when it was true in the world (valid time) and when abagraph learned it (record time). Time-travel queries rewind the whole store to any past instant. Context packets — one call ( POST /api/context or MCP build_context ) returns a token-budgeted, confidence-ranked bundle of the facts most relevant to a task. Load this at agent session start instead of stuffing the prompt. Candidates — a fact below the confidence threshold (default 0.75) lands as Candidate and does not supersede anything. Re-assert at full confidence to promote it. Quickstart: REST API — first five curl calls in 5 minutes What is abagraph? — data model and design intent Agent Integration Overview — LLM memory via MCP or SDK Authentication — bearer token and session cookie auth Glossary — every term defined ### Install the CLI (getting-started) Install the CLI TL;DR Run without installing: npx abagraph help (Node >= 18 required). Persist config: abagraph init --url <URL> --key <KEY> writes ~/.abagraph.json . Config precedence: --flag > ABAGRAPH_URL / ABAGRAPH_KEY env vars > ~/.abagraph.json . Install Run without installing (npx) npx abagraph help Install globally npm install -g @abagraph/cli The CLI is a single zero-dependency JavaScript file ( bin/abagraph.mjs ). It uses the built-in global fetch — no polyfills required. Node.js 18 or later is required. Connect to a server init saves the URL and key to ~/.abagraph.json , checks /api/health , and prints the REST, MCP, and stream endpoints. abagraph init --url https://your-abagraph-host --key <your-token> Wrote ~/.abagraph.json Health: OK REST: https://your-abagraph-host/api MCP: https://your-abagraph-host/mcp Stream: https://your-abagraph-host/api/stream Assert a fact abagraph assert Person:alice works_at Company:acme # write onto a specific branch abagraph assert Person:alice works_at Company:acme --branch staging Output is the full Fact JSON object. Query facts # All active facts abagraph query # Filter by subject or predicate abagraph query --subject Person:alice abagraph query --predicate works_at --limit 5 # Read from a branch abagraph query --subject Person:alice --branch staging Manage branches Branches are instant, isolated graph overlays. Changes on a branch do not affect the main graph until you promote them. abagraph branch create staging abagraph branch list abagraph branch diff staging abagraph branch promote staging abagraph branch discard staging Check for contradictions abagraph factcheck --subject Person:alice --predicate works_at contradictions: 0 [] Print connection snippets connect fetches /api/connect and prints ready-to-paste code snippets for REST, TypeScript, agent memory, and MCP configuration. abagraph connect Generate agent scaffolding agent init fetches the snippets from /api/connect , prints the createAgentMemory and memoryPlugin examples, and writes the MCP server config to ./abagraph.mcp.json . abagraph agent init Consolidate (dedup and surface contradictions) abagraph consolidate collapsed: 3 superseded: 1 conflicts: 0 {...} Environment variables Set these instead of passing flags on every command: export ABAGRAPH_URL=https://your-abagraph-host export ABAGRAPH_KEY=<your-token> Global flags Every command accepts --url and --key to override the stored config for a single invocation. abagraph --url https://acme.abagraph.com --key <token> query --subject Person:sarah All commands init [--url U] [--key K] Save config, check health, show endpoints assert <subject> <predicate> <object> [--branch B] Write a fact query [--subject S] [--predicate P] [--limit N] [--branch B] List facts branch create <name> [--parent P] [--as-of MS] branch list branch diff <name> branch promote <name> branch discard <name> Manage branches connect Print connection snippets factcheck [--subject S] [--predicate P] Show contradictions agent init Print agent/plugin snippets; write ./abagraph.mcp.json consolidate Dedup facts and surface contradictions help Show usage Next steps Authentication — understand bearer tokens, cookie sessions, and subdomain routing Install the TypeScript SDK — programmatic access from Node.js Quickstart: REST API — raw curl equivalents for every command Your First Context Packet — what the context endpoint returns ### Install the TypeScript SDK (getting-started) Install the TypeScript SDK TL;DR Package: @abagraph/client (zero runtime dependencies, Node >= 18). Three entry points: "." (REST client), "./agent" (agent memory), "./plugin" (ADK-style runner hook). Create once with createClient({baseUrl, apiKey}) ; call typed methods. Install npm install @abagraph/client # or yarn add @abagraph/client # or pnpm add @abagraph/client Requires Node.js 18 or later — the SDK uses the built-in global fetch with no polyfills. Create a client export ABAGRAPH_BASE_URL=https://<your-tenant>.abagraph.com export ABAGRAPH_API_KEY=<your-token> import { createClient } from '@abagraph/client' const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, }) // Verify connectivity const health = await db.health() console.log(health.ok) // true Assert and query a fact // Write a fact const fact = await db.assert({ subject: 'Person:alice', predicate: 'works_at', object: 'Company:acme', confidence: 0.95, source: 'onboarding-form', }) console.log(fact.id, fact.status) // "01J..." "active" // Query it back const facts = await db.query({ subject: 'Person:alice' }) console.log(facts[0].object) // "Company:acme" Atomic transact Use transact for compare-and-swap or bulk write operations. A mismatch in compares returns HTTP 409 ( CompareFailed ) and rolls back the entire batch. const result = await db.transact({ compares: [{ subject: 'Person:alice', predicate: 'works_at', object: 'Company:acme' }], asserts: [{ subject: 'Person:alice', predicate: 'works_at', object: 'Company:beta' }], retracts: [], }) console.log(result.tx, result.facts.length) Request a context packet The context method maps directly to POST /api/context and returns a goal-directed, confidence-ranked packet ready for LLM injection. const packet = await db.context({ goal: 'What does alice do professionally?', seeds: ['Person:alice'], max_facts: 20, token_budget: 2048, }) console.log(packet.summary) console.log(`${packet.tokens} / ${packet.total_tokens} tokens used`) for (const f of packet.facts ?? []) { console.log(f.subject, f.predicate, f.object) } Error handling Non-2xx responses throw AbagraphError , which carries the HTTP status code and, when available, the machine-readable code string from the server error envelope. import { AbagraphError } from '@abagraph/client' try { await db.assert({ subject: '', predicate: 'p', object: 'o' }) } catch (err) { if (err instanceof AbagraphError) { console.error(err.status, err.code, err.message) // 400 "SchemaInvalid" "..." } } Agent memory (import ./agent) createAgentMemory wraps the REST client with frozen-snapshot semantics designed for LLM agent loops: loadContext at session start, remember to persist new facts (never reflected in the current snapshot), and turn to run the full load-run-remember cycle in one call. import { createAgentMemory } from '@abagraph/client/agent' const mem = createAgentMemory({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, namespace: 'my-tenant', agent: 'reels-bot', defaultBudget: 4096, onLog: (e) = console.log(e), }) // Option A: fine-grained const ctx = await mem.loadContext('plan a reel for the Milan launch') const result = await runAgent(ctx) await mem.remember([{ subject: 'Campaign:milan', predicate: 'status', object: 'planned', reason: 'agent decided to proceed', }]) // Option B: one-liner turn const out = await mem.turn('plan a reel', async (ctx) = { const result = await runAgent(ctx) return { result, facts: [{ subject: 'Campaign:milan', predicate: 'status', object: 'planned' }] } }) Runner plugin (import ./plugin) memoryPlugin provides ADK-style onSessionStart / onSessionEnd hooks. Install once at the runner; individual agents need no extra wiring. onSessionEnd is fire-and-forget. import { memoryPlugin } from '@abagraph/client/plugin' const plugin = memoryPlugin({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, namespace: 'my-tenant', agent: 'reels-bot', }) const ctx = await plugin.onSessionStart({ task: 'plan a reel' }) // ... run agent with ctx ... plugin.onSessionEnd({ content: transcript }) // async, returns void Next steps Authentication — configure bearer tokens and cookie sessions Install the CLI — command-line access to the same API Your First Context Packet — deeper walkthrough of context assembly Quickstart: REST API — raw curl examples for every step ### Quickstart: REST API (getting-started) Quickstart: REST API TL;DR Five calls: GET /api/health → POST /api/facts → GET /api/facts → POST /api/context → GET /api/stream Every endpoint accepts ?explain=1 for a SQL-style query-plan trace. All write and read endpoints require a bearer token — see Authentication . Prerequisites An abagraph API key and tenant URL. Sign up at abagraph.com or see Authentication . curl available on $PATH . Step 1 — Set your credentials Your base URL is https://<tenant>.abagraph.com . Set it and your API key in your shell so the examples below work unchanged: export ABAGRAPH_BASE_URL=https://<tenant>.abagraph.com export ABAGRAPH_API_KEY=<your-token> Step 2 — Health check GET /api/health is the only endpoint that needs no token. curl -s "$ABAGRAPH_BASE_URL/api/health" {"ok": true} Step 3 — Assert a fact A fact is a subject–predicate–object triple with bitemporal metadata. POST /api/facts returns the stored Fact object with status "active" . curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subject": "Person:alice", "predicate": "works_at", "object": "Company:acme", "confidence": 0.95, "source": "onboarding-form" }' { "id": "01J...", "subject": "Person:alice", "predicate": "works_at", "object": "Company:acme", "confidence": 0.95, "source": "onboarding-form", "status": "active", "valid_from": 1750000000000, "valid_until": null, "tx": 1 } Confidence threshold. The default threshold is 0.7 . Asserting below it produces a "candidate" fact — stored and searchable, but it never supersedes an existing active fact. Reassert at or above the threshold to promote it. Step 4 — Contradict and verify supersession Assert a conflicting value for the same subject + predicate. abagraph automatically closes the old fact ( status: "superseded" ) and opens the new one in one atomic batch. # Alice moves to Beta — supersedes Acme curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:alice","predicate":"works_at","object":"Company:beta"}' # Query: only the new fact is active curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:alice&predicate=works_at" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Time-travel with ?as_of=<unix-milliseconds> to see the old state: curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:alice&as_of=1749999999000" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Step 5 — Query facts GET /api/facts accepts filters for subject , predicate , object , status , as_of , limit (default 200), and tx_gt for cursor-based pagination. # All active facts about alice curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:alice" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Include superseded history curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:alice&status=superseded" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Limit results curl -s "$ABAGRAPH_BASE_URL/api/facts?limit=10" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Step 6 — Request a context packet POST /api/context is the flagship endpoint: a single call that assembles a goal-directed, confidence-ranked, token-budgeted context packet ready for injection into an LLM prompt. curl -s -X POST "$ABAGRAPH_BASE_URL/api/context" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "goal": "What does alice do professionally?", "seeds": ["Person:alice"], "max_facts": 20, "token_budget": 2048 }' { "summary": "Found 1 entity, 2 facts, 0 pages.", "entities": ["Person:alice"], "facts": [...], "pages": [], "tokens": 120, "total_tokens": 240, "collapsed": 1, "corroboration": [...], "warnings": [], "explain": {"steps": []} } The token_budget parameter confidence-ranks facts and trims the set to fit. tokens is the estimated token count of the returned facts; total_tokens is the full candidate set before trimming. Step 7 — Live fact stream GET /api/stream is a Server-Sent Events (SSE) feed. Each event carries data: {"op": "assert"|"retract", "tx": N, "fact": {...}} . A keepalive comment ( :keepalive ) fires roughly every 15 seconds. curl -N \ -H "Accept: text/event-stream" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ "$ABAGRAPH_BASE_URL/api/stream" Bonus — Query-plan traces Add ?explain=1 to any endpoint to get a SQL-style query-plan envelope around the normal result: curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:alice&explain=1" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" { "result": [...], "explain": { "op": "query_facts", "plan": "...", "stats": {"results": 1, "limit_hit": false}, "steps": [{"phase": "execute", "ms": 0}], "total_ms": 1 } } Next steps Authentication — add a bearer token or cookie session Your First Context Packet — deeper walkthrough of the context endpoint Install the TypeScript SDK — typed client for Node.js Install the CLI — interactive command-line access ### Getting Started (getting-started) Getting Started TL;DR Get your tenant URL and API key, then call POST /api/facts , POST /api/context , and POST /mcp . abagraph is a fully managed service — no installation or infrastructure required. All endpoints accept ?explain=1 for a SQL-style query-plan trace. 1 · Get your API key Sign up at abagraph.com to provision a tenant and receive an API key. Your base URL is https://<tenant>.abagraph.com . Set both in your shell for the examples below: export ABAGRAPH_BASE_URL=https://<tenant>.abagraph.com export ABAGRAPH_API_KEY=<your-token> 2 · Assert your first fact A fact is a subject–predicate–object triple with bitemporal metadata. POST /api/facts stores it and returns the full Fact object with status: "active" . curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subject": "Person:alice", "predicate": "works_at", "object": "Company:acme", "confidence": 0.95, "source": "onboarding-form" }' { "id": "01J...", "subject": "Person:alice", "predicate": "works_at", "object": "Company:acme", "status": "active", "valid_from": 1750000000.0, "valid_until": null } 3 · Query it back curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:alice" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Add ?explain=1 to see index selection and query timings: curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:alice&explain=1" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" 4 · Get a context packet POST /api/context assembles a goal-directed, confidence-ranked, token-budgeted packet ready for injection into an LLM prompt — one call replaces manual retrieval and ranking logic. curl -s -X POST "$ABAGRAPH_BASE_URL/api/context" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "goal": "What does alice do professionally?", "seeds": ["Person:alice"], "max_facts": 20, "token_budget": 2048 }' 5 · Connect via MCP abagraph exposes seven MCP tools — digest , remember , recall , recall_as_of , whats_changed , build_context , explain_fact — at POST /mcp . Add the endpoint to your MCP client configuration: { "mcpServers": { "abagraph": { "url": "https://<tenant>.abagraph.com/mcp", "headers": { "Authorization": "Bearer <your-token>" } } } } First request with query-plan trace curl -s "$ABAGRAPH_BASE_URL/api/facts?explain=1" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | head -c 400 Every endpoint wraps its response in {result, explain} when ?explain=1 is appended. Errors always come back as {"error":{"code","message"}} . Common mistakes Omitting the Authorization header — the API returns 404 NotFound (not 401) when auth fails. See Authentication Failures . Using a token scoped to the wrong tenant on a subdomain request — tenant tokens are validated against the subdomain label. Expecting semantic search results immediately after asserting facts when no embedding provider is active — check for abagraph.embedding.disabled in your logs. See Nothing Recalled . Next steps What is abagraph? — the elevator pitch Mental Model — facts, bitemporality, supersede Quickstart: REST API — full five-call walkthrough ### Your First Context Packet (getting-started) Your First Context Packet TL;DR POST /api/context is the single call that assembles everything an LLM needs: entities, facts, wiki pages, token counts, and corroboration signals. Pass a goal (your prompt task), optional seeds (entity anchors), and a token_budget to get a trimmed, confidence-ranked packet. The packet is deterministic and offline by default — no LLM is called during assembly. What is a context packet A context packet is a bounded, provenance-rich snapshot of the knowledge graph assembled for a specific goal. It combines: entities — the entity references touched during assembly. facts — the top-ranked active facts for those entities, with corroboration signals. pages — wiki page snippets whose content is relevant to the goal. token estimates — so you know what fraction of your LLM context window the packet occupies. The assembly pipeline runs index lookups, a 1-hop graph walk from the seed entities, a kNN semantic search over the goal text, deduplication, confidence ranking, and optional token-budget trimming — all in one synchronous call with no external dependencies. Prerequisites A running abagraph server with at least one asserted fact. See Quickstart: REST API . The environment variables ABAGRAPH_BASE_URL and optionally ABAGRAPH_API_KEY . The request curl -s -X POST "$ABAGRAPH_BASE_URL/api/context" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -d '{ "goal": "What does alice do professionally and who does she collaborate with?", "seeds": ["Person:alice"], "max_facts": 20, "max_pages": 4, "token_budget": 2048 }' Request fields Field Type Default Description goal string required The task or question driving context assembly. Used for kNN semantic search. seeds string[] [] Entity references ( "Type:id" ) to anchor the graph walk. max_facts number 64 Maximum number of facts to return (count cap, applied before token trimming). max_pages number 8 Maximum wiki page snippets to include. token_budget number none When set, facts are confidence-ranked and trimmed to fit this many tokens. Enables tokens / total_tokens in the response. as_of number now Unix timestamp (seconds). Returns the world-state as it was at that point in time. project string none Optional project entity reference to narrow the walk. cluster_threshold number 0.0 Cosine similarity threshold for near-duplicate clustering after dedup. 0.0 disables clustering. A typical value is 0.97 . compact_model object none BYOK model config for semantic tail-compaction. Omitting keeps the pipeline deterministic and offline. The response { "kind": "context_packet", "summary": "Found 2 entities, 5 facts, 1 page.", "entities": ["Person:alice", "Company:acme"], "facts": [ { "id": "01J...", "subject": "Person:alice", "predicate": "works_at", "object": "Company:acme", "confidence": 0.95, "status": "active", "valid_from": 1750000000.0, "valid_until": null } ], "pages": [ { "id": "01K...", "subject": "Company:acme", "title": "Acme Corp", "body": "Acme Corp is a fictional company used in examples.", "source": "wiki" } ], "tokens": 420, "total_tokens": 890, "collapsed": 1, "corroboration": [ { "agree": 2, "conflict": 0, "sources": ["onboarding-form", "hr-sync"] } ], "derived_facts": [], "warnings": [], "explain": { "steps": [] } } Response fields Field Description summary Human-readable one-line description of what was found. entities Entity references touched during assembly. facts The selected active facts, confidence-ranked if token_budget was set. pages Wiki page snippets. Each has id , subject , title , body , source . tokens Estimated token count of the returned facts. Present when token_budget was set. total_tokens Token count of the full candidate set before trimming. total_tokens - tokens is the reduction achieved. collapsed Number of duplicate or near-duplicate facts removed by dedup and clustering. corroboration Per-fact consensus signals, index-aligned with facts . agree is the count of sources that confirm; conflict is the count of contradicting sources. derived_facts Facts synthesized from the dropped tail by BYOK tail-compaction. Empty on the default offline path. warnings Non-fatal issues encountered during assembly (e.g. a seed entity not found). explain Assembly pipeline steps. Expanded further with ?explain=1 . Inject the packet into an LLM prompt The packet is designed for direct injection. Serialize the facts as a numbered list and include the summary as a preamble: import { createClient } from '@abagraph/client' const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY }) const packet = await db.context({ goal: 'What does alice do professionally?', seeds: ['Person:alice'], token_budget: 2048, }) const contextBlock = (packet.facts ?? []) .map((f, i) = `${i + 1}. ${f.subject} ${f.predicate} ${f.object} (confidence=${f.confidence ?? '?'})`) .join('\n') const systemPrompt = `You are an assistant with access to the following verified knowledge:\n\n${contextBlock}\n\n${packet.summary}` // ... pass systemPrompt to your LLM of choice Historical snapshots with as_of Pass as_of (Unix seconds) to assemble a packet from a past world-state. Only facts that were active at that timestamp are included — superseded facts that came after are excluded, and facts that were later retracted are reinstated. curl -s -X POST "$ABAGRAPH_BASE_URL/api/context" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -d '{"goal": "alice employment history", "seeds": ["Person:alice"], "as_of": 1749999999}' Context via MCP The same assembly logic is exposed as the build_context MCP tool. LLM agents using the MCP server call it with a task instead of a goal and get the same packet format. See the Quickstart for how to configure the MCP server. Common mistakes Empty context with many facts in the graph. Check that the seeds entities exist and are spelled correctly ( "Person:alice" , not "person:alice" ). Entity references are case-sensitive. tokens: 0 always. Token counting requires token_budget to be set. Without it, tokens and total_tokens default to 0 . No semantic search results. Semantic kNN over the goal text requires an embedding provider. Set ABAGRAPH_EMBEDDING=openai (or hash for development) and the corresponding key. See Authentication for env var details. Next steps Quickstart: REST API — the full curl flow from scratch Install the TypeScript SDK — typed db.context() and agent memory wrappers Authentication — add a token and enable tenant scoping Install the CLI — command-line access to assert and query ### The Graph Model (graph) The Graph Model TL;DR Every entity is an EntityRef string in the form "Type:id" . A fact whose object is another entity string is an edge; all other facts are properties. GET /api/graph materialises nodes and edges on-demand from active facts — there is no separate graph schema to define. Definition abagraph is a bitemporal graph + vector memory engine . The graph view is not a separate data structure: it is a projection over the fact store. Every fact that links one entity to another becomes an edge; every fact that attaches a scalar value to an entity becomes a property. You model domain knowledge once, as facts, and the graph emerges automatically. Why it matters Traditional graph databases require you to design a schema up-front: node types, relationship types, property keys. abagraph inverts this: assert facts first, query the graph whenever you need it. Time-travel is built in — the graph as it existed at any past timestamp is always recoverable via as_of . Mental model Think of every entity in your domain as a labelled dot. Two dots are connected by an arrow whenever a fact asserts that one refers to another. The arrow carries the predicate name as its label. Person:alice ──[works_at]──► Org:acme Person:alice ──[reports_to]──► Person:bob Org:acme ──[headquartered_in]──► City:sf Facts with non-entity objects (numbers, booleans, text that is not an entity ref) are attribute facts. They attach to the node but do not create an edge arrow. Entities An entity is identified by an EntityRef — a plain string. The conventional format is "Type:id" : a type token, a colon, and a local identifier. Examples: Person:alice Org:acme Concept:bitemporal Project:llm_wiki The type prefix is parsed by the server to populate the kind field in graph responses. An entity ref without a recognisable type prefix (e.g. a bare UUID or an ISO timestamp) is still a valid node; its kind will be null and its name will be the full string. Entities do not need to be declared. They come into existence the first time a fact references them as a subject or object. The entity response groups all facts for one entity together: { "id": "Person:alice", "kind": "Person", "facts": [ { "predicate": "works_at", "object": "Org:acme", "status": "Active" } ] } Retrieve an entity and all its facts at GET /api/entities/:id . Edges A fact becomes an edge when its object is a text value that can be interpreted as an entity ref. Each edge in the walk and graph API responses carries the underlying fact's full provenance — the fact id, endpoints, predicate name, confidence, source, and timestamps: { "id": "01J...", "from": "Person:alice", "to": "Org:acme", "predicate": "works_at", "confidence": 0.95, "source": "hr-system" } There is no additional storage cost for edges: from , to , and predicate are derived from the fact at query time. The graph endpoint The GET /api/graph endpoint materialises the full graph (or a focused subgraph) from active facts in the current tenant scope. Query parameters Parameter Default Description q — Focus string. Returns matching nodes and their 1-hop neighbourhood. Limits the result to 80 nodes by default. limit 0 (all) Cap on nodes returned, ranked by degree + fact count. 0 means no cap. When q is set, the cap is at most 80. Response shape { "nodes": [ { "id": "Person:alice", "kind": "Person", "name": "alice", "degree": 3, "fact_count": 5 } ], "edges": [ { "id": "01J...", "from": "Person:alice", "to": "Org:acme", "predicate": "works_at", "confidence": 0.95 } ], "total_nodes": 42, "total_edges": 61, "limited": false } When limited is true , the response contains the top- limit nodes ranked by importance ( degree + fact_count ); total_nodes and total_edges report the uncapped counts. Search seeds (nodes matched by q ) are always preserved, regardless of their degree rank. curl example # Full graph (small datasets) curl -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ "$ABAGRAPH_BASE_URL/api/graph" # Focus on everything connected to "alice" within one hop curl -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ "$ABAGRAPH_BASE_URL/api/graph?q=alice&limit=40" TypeScript SDK example import { createClient } from "@abagraph/client"; const client = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, }); const graph = await client.graph(); console.log(`${graph.total_nodes} nodes, ${graph.total_edges} edges`); Fact status and the graph Only Active facts appear in the graph view returned by /api/graph . Superseded , Retracted , and Candidate facts are invisible unless you use the /api/walk endpoint with as_of to request a historical snapshot. Trade-offs Schema-free. No migration needed when you add a new predicate or entity type. The trade-off is that the engine cannot enforce referential integrity; dangling references are possible and surfaced by a consistency lint that flags orphans and dangling references. Edges are facts. Every edge carries confidence, source, and a validity interval. This makes provenance tracking straightforward but means deleting a relationship requires a retract, not a foreign-key drop. Text-object convention. Any text object that looks like an entity ref participates in the graph. If you want to store a plain string that happens to contain a : — like a URL or an ISO timestamp — it will not be treated as an edge because the engine validates that the prefix is a real type token (all ASCII letters/digits/underscores, starting with a letter). Walking the Graph — BFS traversal with filters Wiki Links and Citations — auto-emit graph edges from narrative pages Mental Model — overall bitemporal architecture Guide: Assert a Fact — write your first fact/edge ### Walking the Graph (graph) Walking the Graph TL;DR GET /api/walk?start=<entity> runs a BFS from one entity, returning all reached nodes and traversed edges. Default: depth 2, outbound direction, all edge types, current time. Pass as_of to walk the graph as it existed at any past timestamp. How walk works Walk performs a breadth-first search (BFS) starting from a single entity. At each step, it expands the frontier by loading the facts whose subject (outbound) or object (inbound) matches each frontier node, applies the configured filters, and emits each new edge exactly once. The search stops when the frontier is empty or the depth limit is reached. Only facts whose object is an entity ref string participate in traversal. Facts with scalar objects (numbers, booleans, blobs) are invisible to the walk — they are attribute facts, not edges. Retracted facts are never traversed. Without as_of , only Active facts are followed; with as_of , the bitemporal validity filter selects which facts were active at that moment in world-time. Walk options Direction values: Value Meaning out (default) Follow edges where the start node is the subject — forward traversal. in Follow edges where the start node is the object — reverse traversal. both Follow edges in either direction. REST: GET /api/walk query parameters Parameter Required Default Description start Yes — Starting entity ref, e.g. Person:alice . depth No 2 BFS depth limit. direction No out out , in , or both . edge_types No all Comma-separated list of predicate names to follow, e.g. works_at,reports_to . as_of No now Unix millisecond timestamp for time-travel. Response shape { "start": "Person:alice", "reached": ["Person:alice", "Org:acme", "Person:bob", "City:sf"], "edges": [ { "id": "01J...", "from": "Person:alice", "to": "Org:acme", "predicate": "works_at", "fact": { "id": "01J...", "subject": "Person:alice", "predicate": "works_at", "object": "Org:acme", "confidence": 0.95, "status": "active", ... } } ] } reached includes the start node plus every node discovered during BFS. edges contains each traversed fact exactly once (duplicates across BFS layers are deduplicated by fact id). The full fact is embedded in each edge so confidence, source, and timestamps are available without a second query. Time-travel walk Pass as_of to reconstruct the graph at any past moment. The BFS applies the bitemporal validity filter: a fact participates if its validity interval [valid_from, valid_until) contained the requested timestamp. REST examples # Default walk from alice (depth 2, outbound, all edge types) curl -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ "$ABAGRAPH_BASE_URL/api/walk?start=Person:alice" # Depth-3 bidirectional walk, only 'references' and 'cites' edges curl -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ "$ABAGRAPH_BASE_URL/api/walk?start=Person:karpathy&depth=3&direction=both&edge_types=references,cites" # Time-travel: graph as it existed at a past Unix-ms timestamp curl -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ "$ABAGRAPH_BASE_URL/api/walk?start=Person:alice&as_of=1718000000000" TypeScript SDK example import { createClient } from "@abagraph/client"; const client = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, }); const result = await client.walk({ start: "Person:alice", depth: 3, direction: "both", edge_types: "works_at,reports_to", }); for (const edge of result.edges as any[]) { console.log(`${edge.from} --[${edge.predicate}]-- ${edge.to}`); } Tenant isolation. The managed service automatically scopes the BFS to the authenticated tenant. Cross-tenant edges are dropped inside the BFS — they are never post-filtered, so reached and edges will never expose another tenant's data. Query plan telemetry Append ?explain=1 to any walk request to receive the BFS plan and per-phase latency alongside the result: curl -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ "$ABAGRAPH_BASE_URL/api/walk?start=Person:alice&explain=1" The response wraps the normal payload in { "result": {...}, "explain": { "op", "plan", "stats": {"reached", "edges_emitted"}, "steps": [...], "total_ms" } } . Common mistakes No edges returned. Facts with scalar objects are not edges. Check that the object value is a string in "Type:id" form, not a number or plain text. Unexpected results with deep depth. A high depth value on a dense graph can return a very large result. Start with the default ( 2 ) and increase only when needed. Use edge_types to narrow the traversal. Superseded facts visible in walk. Without as_of only Active facts are followed. To see edges that were valid at a past time, supply as_of . start missing. The endpoint returns 400 SchemaInvalid if start is omitted. The Graph Model — nodes, edges, and how the graph is derived Wiki Links and Citations — auto-emit edges via wiki markup Guide: Time-Travel Queries — bitemporal as_of patterns Mental Model — bitemporal fact architecture ### Wiki Links and Citations (graph) Wiki Links and Citations TL;DR Write narrative pages in markdown-style text; [[Entity:foo]] links auto-become references edges in the graph. Attach a Citation to a page and it lands as a cites fact with the quoted text in properties.quote . All wiki-derived facts share the same source as the page, so provenance roundtrips. Overview abagraph includes an optional wiki layer that lets you write human-readable narrative pages about entities. When you save a page, the engine extracts structured signal automatically: every [[wiki-link]] in the body becomes a references edge, and every Citation you attach becomes a cites fact carrying the source URL and a verbatim quote. Nothing in the wiki layer stores data differently — a page is just a cluster of facts written in one Wiki::save() call. Every wiki-derived fact is bitemporally versioned like any other fact: you can travel back to any past snapshot of a page and see exactly which links and citations existed at that moment. The page entity A wiki page is represented as a cluster of facts anchored to a page entity. The conventional page id is "Page:{subject}" , where subject is the entity the page describes. Four predicates describe a page: wiki_page — links the subject entity to its page id. title — the display title, stored as a literal string. wiki_body — the raw narrative body (wiki-link markup is preserved in the stored text). source — a provenance label inherited by all facts derived from this page. Defaults to "wiki:{page_id}" when omitted. Wiki link syntax Two forms are supported: [[Target]] — target is the entity ref, display text is the target string. [[Target|display text]] — target is before the pipe; display text is after it. The target is taken verbatim and becomes the object of the references fact. Entity types are conventional — [[Person:sarah]] , [[Org:acme|Acme Corp]] , [[Concept:bitemporal]] are all valid. Unterminated links (missing ]] ) are ignored. Duplicate links in the same body are each stored individually (the references predicate uses ConflictPolicy::Coexist , so multiple active edges between the same pair of entities are permitted). What saving a page writes One save operation asserts up to four categories of facts, all in the same transaction batch: wiki_page edge — links the subject entity to the page id: subject --[wiki_page]--> page.id wiki_body literal — stores the raw body text on the page id: page.id --[wiki_body]--> body title literal — stored when the title is non-empty: page.id --[title]--> title references edges — one per [[wiki-link]] , from the subject entity to the link target, with ConflictPolicy::Coexist . cites literals — one per Citation , storing the URL as the object and the verbatim quote in properties.quote , with ConflictPolicy::Coexist . All of these facts carry the page's source label (defaulting to "wiki:{page.id}" if none is supplied). REST example Assert a wiki page for Karpathy and its references edges in one atomic batch via POST /api/transact . References edges are asserted explicitly for each [[wiki-link]] in the body: curl -X POST -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "asserts": [ {"subject":"Person:karpathy","predicate":"wiki_page","object":"Page:Person:karpathy","source":"wikipedia:Andrej_Karpathy"}, {"subject":"Page:Person:karpathy","predicate":"title","object":"Andrej Karpathy","source":"wikipedia:Andrej_Karpathy"}, {"subject":"Page:Person:karpathy","predicate":"wiki_body","object":"Andrej co-founded Org:openai and led AI at Org:tesla. He is now working on Project:llm_wiki.","source":"wikipedia:Andrej_Karpathy"}, {"subject":"Person:karpathy","predicate":"references","object":"Org:openai","conflict_policy":"Coexist","source":"wikipedia:Andrej_Karpathy"}, {"subject":"Person:karpathy","predicate":"references","object":"Org:tesla","conflict_policy":"Coexist","source":"wikipedia:Andrej_Karpathy"}, {"subject":"Person:karpathy","predicate":"references","object":"Project:llm_wiki","conflict_policy":"Coexist","source":"wikipedia:Andrej_Karpathy"} ] }' \ 'https://<tenant>.abagraph.com/api/transact' Walk the references graph from Karpathy at depth 2: curl -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ 'https://<tenant>.abagraph.com/api/walk?subject=Person%3Akarpathy&predicate=references&depth=2' # Response edges: # Person:karpathy --[references]-- Org:openai # Person:karpathy --[references]-- Org:tesla # Person:karpathy --[references]-- Project:llm_wiki # Org:openai --[references]-- Person:karpathy Reading outgoing references Query all references targets for an entity with a pattern query: curl -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ 'https://<tenant>.abagraph.com/api/facts?subject=Person%3Akarpathy&predicate=references' # Returns facts whose objects are: "Org:openai", "Org:tesla", "Project:llm_wiki" Citations A citation records a source URL and a verbatim quote. Assert it as a cites fact on the page entity, with the URL as the object and the verbatim quote in properties.quote : curl -X POST -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subject": "Page:Person:karpathy", "predicate": "cites", "object": "https://karpathy.ai", "properties": { "quote": "AI researcher and educator." }, "source": "wikipedia:Andrej_Karpathy", "conflict_policy": "Coexist" }' \ 'https://<tenant>.abagraph.com/api/facts' The stored fact looks like: { "subject": "Page:Person:karpathy", "predicate": "cites", "object": "https://karpathy.ai", "properties": { "quote": "AI researcher and educator." }, "source": "wikipedia:Andrej_Karpathy", "status": "active" } Multiple citations on the same page are all stored ( ConflictPolicy::Coexist ). You can query all citations for a page with a standard pattern query on the cites predicate. Page versioning Because every wiki-derived fact is a standard bitemporal fact, saving a page a second time with changed content behaves like any other supersession cycle: The wiki_body and title facts are functional predicates (one active value per entity) — the old values are closed and the new values open in one atomic batch. The references and cites facts use Coexist and accumulate. If you remove a link from the body and re-save, the old references fact is NOT automatically closed. To retract a link, call aba.retract(fact_id) explicitly after locating it with a query. To recover the body of a page as it existed at a past timestamp, query with as_of(timestamp) filtered to the wiki_body predicate on the page id. Common mistakes Links not becoming edges. The walk only follows text objects that look like entity refs. If the link target contains spaces or does not follow "Type:id" conventions, it is still stored as a references fact but will not appear as a walkable edge. Use a clean entity ref as the target and a display label after the pipe: [[Person:sarah|Sarah Jones]] . Old references lingering after a page update. Re-saving a page does not retract old references edges. Retract them manually if you need a clean diff. The Graph Model — entities, edges, and the graph view Walking the Graph — traverse references and cites edges Guide: Assert a Fact — the underlying fact assert API ### Guide: Assert a Fact (guides) Guide: Assert a Fact TL;DR A fact is a subject — predicate → object triple with a confidence score and a validity interval. Send POST /api/facts to write; a conflicting assert automatically closes the old fact ( Superseded ). Add ?as_of=<unix-ms> to any read to see the world as it stood at that moment. What is a fact Every piece of knowledge in abagraph is stored as a fact : a subject-predicate-object triple annotated with a confidence score, a validity interval, a transaction timestamp, and an optional source. For example: Person:sarah --works_at-- Org:acme (confidence: 0.95, source: "onboarding") subject and object follow the Type:id convention; predicate is a free-form lowercase string. A fact whose confidence is at or above the threshold (default 0.75) is stored as Active ; below it lands as Candidate — stored and searchable, but it does not supersede an existing Active fact. Assert a fact curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme","source":"onboarding","confidence":0.95}' \ | jq . Response (HTTP 201): { "id": "01J...", "subject": "Person:sarah", "predicate": "works_at", "object": "Org:acme", "status": "Active", "confidence": 0.95, "source": "onboarding", "valid_from": 1720000000000, "valid_until": null, "tx": 1 } Confidence threshold The default threshold is 0.75 . Assert below it and the fact lands as Candidate — stored, searchable, but it never supersedes an existing Active fact. Reassert at or above the threshold to promote it. Query it back curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:sarah" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '[.[] | {predicate,object,status}]' # [{"predicate":"works_at","object":"Org:acme","status":"Active"}] Contradict and supersede Assert a different object for the same subject and predicate. The old fact closes atomically: curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:beta"}' | jq . Now only the Beta fact is Active . The Acme fact is Superseded but preserved in the bitemporal log. Query it with ?status=Superseded or the ?as_of=<unix-ms> parameter. Set-valued predicates Use "policy":"coexist" to keep multiple objects active for the same subject and predicate: curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"tag","object":"engineer","policy":"coexist"}' | jq . curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"tag","object":"lead","policy":"coexist"}' | jq . Both tags remain Active . Without "policy":"coexist" , the second assert would supersede the first. Next steps Guide: Assert and Query Facts — full REST walkthrough with pagination and filters Guide: Atomic Transactions — compare-guarded batch writes Guide: Time-Travel Queries — query superseded history with as_of API Reference: /api/facts — complete field reference ### Guide: Assert and Query Facts (guides) Guide: Assert and Query Facts TL;DR POST /api/facts writes one triple; GET /api/facts reads them back with optional filters. A conflicting assert for the same subject+predicate closes the old fact atomically ( Superseded ) and opens a new one. Default limit is 200 ; page deeper with ?tx_gt=<cursor> . Prerequisites ABAGRAPH_BASE_URL and ABAGRAPH_API_KEY set in your environment (available from the abagraph console under your tenant settings). Assert a fact over REST Send a JSON body with subject , predicate , and object . All other fields are optional. curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme","source":"onboarding","confidence":0.95}' \ | jq . Response (HTTP 201): { "id": "01J...", "subject": "Person:sarah", "predicate": "works_at", "object": "Org:acme", "status": "Active", "confidence": 0.95, "source": "onboarding", "valid_from": 1720000000000, "valid_until": null, "tx": 1 } Query facts over REST Filter by any combination of subject , predicate , object , and status : # All facts for one entity curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:sarah" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq . # Narrow to a predicate curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:sarah&predicate=works_at" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq . Default limit is 200. Results are not paginated automatically. For larger datasets pass ?limit=N (up to 200) or use the tx_gt cursor: GET /api/facts?tx_gt=<last_tx> to resume from a known write-transaction number. Assert and query with the TypeScript SDK import { createClient } from "@abagraph/client"; const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, }); // Assert const fact = await db.assert({ subject: "Person:sarah", predicate: "works_at", object: "Org:acme", source: "onboarding", confidence: 0.95, }); console.log(fact.id, fact.status); // "01J..." "Active" // Query const facts = await db.query({ subject: "Person:sarah" }); console.log(facts.map((f) = `${f.predicate} → ${f.object}`)); Contradiction: automatic supersede Asserting a different object for the same subject+predicate closes the old fact in the same atomic write. The engine applies AutoSupersede by default: # Sarah moves to Beta — Acme is now Superseded curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:beta"}' | jq . Now GET /api/facts?subject=Person:sarah&predicate=works_at returns only the Beta fact with status Active . The Acme fact is still in history — query it with ?status=Superseded or use the time-travel guide . Set-valued predicates Use "policy":"coexist" to keep multiple objects active for the same subject+predicate. Typical set-valued predicates: tag , references , cites , member_of . curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"tag","object":"engineer","policy":"coexist"}' | jq . curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"tag","object":"lead","policy":"coexist"}' | jq . Both tags remain Active . Retract a fact curl -s -X DELETE "$ABAGRAPH_BASE_URL/api/facts/01J..." \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq . # {"ok":true,"id":"01J..."} Retracted facts are hidden from default queries but remain in the bitemporal log. Pass ?status=Retracted to see them. Verify curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:sarah&predicate=works_at" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '.[].status' # "Active" Common mistakes Missing subject or predicate. Both are required; the server returns 400 SchemaInvalid . Expecting immediate supersede after a low-confidence assert. Facts below the confidence threshold (default 0.7 ) land as Candidate and do NOT supersede an existing Active fact. Forgetting "policy":"coexist" on set-valued predicates. Without it, the second tag replaces the first. Guide: Assert a Fact — triple model intro and concept reference Guide: Atomic Transactions — compare-guarded batch writes Guide: Time-Travel Queries — query superseded history Guide: Fact-Check Contradictions — surface conflicts ### Guide: Atomic Transactions (guides) Guide: Atomic Transactions TL;DR POST /api/transact evaluates compares , then applies asserts and retracts in one linearized batch. If any compare guard fails the whole call returns 409 CompareFailed with a failures array — nothing is written. Concurrent claimers race; exactly one wins per guard set. When to use transactions POST /api/facts is fine for independent writes. Use /api/transact when you need to: Write multiple facts that must all succeed or all fail. Guard a write on the current state of an existing fact (optimistic concurrency / compare-and-swap). Retract one fact and assert another atomically. Request shape { "compares": [ { "subject": "Person:sarah", "predicate": "works_at", "object": "Org:acme" } ], "asserts": [ { "subject": "Person:sarah", "predicate": "works_at", "object": "Org:beta", "source": "hr-system" } ], "retracts": [] } compares — array of {subject, predicate, object, tenant?} . Each guard checks that the named triple is currently Active . All guards must pass. asserts — array of FactInput objects (same shape as POST /api/facts ). retracts — array of fact IDs (strings) to retract. All three fields default to empty arrays; an empty compares makes the call unconditionally atomic. Minimal working example # 1. Seed the fact we'll guard against curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme"}' | jq . # 2. Transact: change employer only if Acme is still active curl -s -X POST "$ABAGRAPH_BASE_URL/api/transact" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "compares": [{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme"}], "asserts": [{"subject":"Person:sarah","predicate":"works_at","object":"Org:beta","source":"hr"}], "retracts": [] }' | jq . Successful response (HTTP 200): { "tx": 2, "facts": [ { "id": "01J...", "subject": "Person:sarah", "predicate": "works_at", "object": "Org:beta", "status": "Active", "tx": 2 } ], "retracted": [] } What happens when a guard fails Run the same call again — Org:acme is now Superseded , so the guard fails: curl -s -X POST "$ABAGRAPH_BASE_URL/api/transact" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "compares": [{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme"}], "asserts": [{"subject":"Person:sarah","predicate":"works_at","object":"Org:gamma"}], "retracts": [] }' | jq . Response (HTTP 409): { "error": { "code": "CompareFailed", "message": "compare failed: 1 guard(s) did not match current state", "failures": [ { "subject": "Person:sarah", "predicate": "works_at", "expected": "Org:acme", "active": ["Org:beta"] } ] } } The failures array includes the current active values so you can decide whether to retry or abort — no extra round-trip needed. TypeScript SDK import { createClient, AbagraphError } from "@abagraph/client"; const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, }); try { const result = await db.transact({ compares: [{ subject: "Person:sarah", predicate: "works_at", object: "Org:acme" }], asserts: [{ subject: "Person:sarah", predicate: "works_at", object: "Org:beta", source: "hr" }], retracts: [], }); console.log("tx", result.tx, "facts written:", result.facts.length); } catch (err) { if (err instanceof AbagraphError && err.code === "CompareFailed") { console.error("lost the race — retry with fresh read", err.message); } else { throw err; } } Multi-fact atomic write without guards Omit compares for an unconditional atomic batch (all facts share the same tx ): curl -s -X POST "$ABAGRAPH_BASE_URL/api/transact" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "asserts": [ {"subject":"Person:sarah","predicate":"role","object":"VP Engineering"}, {"subject":"Person:sarah","predicate":"location","object":"Tel Aviv"}, {"subject":"Person:sarah","predicate":"start_date","object":"2024-01-15"} ] }' | jq '.tx, (.facts | length)' Retract and assert atomically FACT_ID=$(curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Project:atlas&predicate=status" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq -r '.[0].id') curl -s -X POST "$ABAGRAPH_BASE_URL/api/transact" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"asserts\": [{\"subject\":\"Project:atlas\",\"predicate\":\"status\",\"object\":\"shipped\"}], \"retracts\": [\"$FACT_ID\"] }" | jq . PII handling PII auto-redaction runs on every assert in a /api/transact call before embedding or persistence — the same chokepoint as POST /api/facts . Verify curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:sarah&predicate=works_at" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '.[].object' # "Org:beta" Common mistakes Guarding on a Superseded or Candidate fact. Compares match only Active facts. If the triple you guard on was superseded by an earlier write, the guard will fail immediately. Using transact for single-fact idempotent writes. POST /api/facts is simpler for single-fact writes where you do not need compare-and-swap. Retrying without re-reading the current state. After a CompareFailed , read the failures[].active field to learn the current state before deciding whether to retry. Guide: Assert and Query Facts — single-fact REST writes Guide: Time-Travel Queries — inspect superseded history Guide: Fact-Check Contradictions — surface conflicting active facts ### Guide: Branch and Merge (guides) Guide: Branch and Merge TL;DR A branch is an instant as_of overlay — fork is free, no data is copied. Branch writes live in a derived tenant so they never mutate or supersede parent facts. Promote copies overlay facts onto the parent; discard removes the overlay. Both close the branch. What is a branch Branching in abagraph is modelled after Neon's instant database branching. When you create a branch you pin a base_ts (the fork point) and get an isolated overlay tenant. Reads reconcile the overlay with the parent snapshot: overlay writes win, parent facts fill the gaps. Promoting merges the overlay back; discarding deletes the overlay. The parent is never touched during branch writes. Branch names must be ASCII alphanumeric, hyphen, or underscore; maximum 64 characters; not main . Prerequisites ABAGRAPH_BASE_URL and ABAGRAPH_API_KEY set in your environment. A few facts already ingested so the parent tenant has data to branch from. Step 1 — Create a branch curl -s -X POST "$ABAGRAPH_BASE_URL/api/branches" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"feature-q3","parent":"main"}' | jq . Response (HTTP 201): { "name": "feature-q3", "parent": "main", "status": "open", "base_ts": 1720000000000, "created_at": 1720000000000 } Pass as_of (Unix ms) to pin the fork to a past point in time: curl -s -X POST "$ABAGRAPH_BASE_URL/api/branches" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"name\":\"debug-snapshot\",\"as_of\":$(( $(date +%s%3N) - 3600000 ))}" | jq . Step 2 — Write to the branch Use ?branch=NAME on POST /api/facts to write to the branch overlay. These writes do not touch the parent: curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts?branch=feature-q3" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Project:atlas","predicate":"status","object":"in-review","source":"branch-test"}' | jq . Step 3 — Read the reconciled view Use ?branch=NAME on GET /api/facts to read the overlay merged with the parent snapshot: curl -s "$ABAGRAPH_BASE_URL/api/facts?branch=feature-q3&subject=Project:atlas" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '[.[] | {predicate,object}]' The branch fact for status shadows any parent fact for the same subject+predicate. Other predicates from the parent snapshot are still visible. Step 4 — Diff against the parent curl -s "$ABAGRAPH_BASE_URL/api/branches/feature-q3/diff" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '{branch,counts,added:[.added[]|{subject,predicate,object}]}' Response includes added , changed , and removed arrays plus counts: { "branch": "feature-q3", "parent": "main", "base_ts": 1720000000000, "added": [], "changed": [{"subject":"Project:atlas","predicate":"status","object":"in-review"}], "removed": [], "counts": {"added":0,"changed":1,"removed":0} } Step 5a — Promote (merge into parent) Promote copies all active overlay facts onto the parent tenant, stripping branch metadata. The branch status changes to promoted and it is no longer writable: curl -s -X POST "$ABAGRAPH_BASE_URL/api/branches/feature-q3/promote" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq . # {"ok":true,"branch":"feature-q3","promoted":1} After promotion the parent contains the branch's Project:atlas status in-review fact and the old parent fact for that predicate is superseded. Step 5b — Discard (delete the overlay) Discard retracts all overlay facts and closes the branch. The parent is not touched: curl -s -X POST "$ABAGRAPH_BASE_URL/api/branches/feature-q3/discard" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq . # {"ok":true,"branch":"feature-q3","discarded":1} Alternatively, DELETE /api/branches/:name is an alias for discard. TypeScript SDK import { createClient } from "@abagraph/client"; const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, }); // Create const branch = await db.branches.create({ name: "feature-q3" }); // Write (uses assert with branch option) await db.assert( { subject: "Project:atlas", predicate: "status", object: "in-review" }, { branch: "feature-q3" }, ); // Read reconciled view const facts = await db.query({ subject: "Project:atlas", branch: "feature-q3" }); // Diff const diff = await db.branches.diff("feature-q3"); // Promote await db.branches.promote("feature-q3"); // Or discard // await db.branches.discard("feature-q3"); List branches curl -s "$ABAGRAPH_BASE_URL/api/branches" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '[.[] | {name,status,base_ts}]' Verify # After promote: parent should reflect the branch's status curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Project:atlas&predicate=status" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '.[].object' # "in-review" Common mistakes Writing to a non-open branch. Promoted and discarded branches return 409 Conflict : "branch is not open". Invalid branch name. Names must be ASCII alphanumeric, hyphen, or underscore; max 64 characters; not main . Other characters return 400 SchemaInvalid . Expecting promote to retract parent facts first. Promote asserts overlay facts onto the parent via the normal assert path, which closes conflicting parent facts via AutoSupersede. It does not explicitly retract. Guide: Time-Travel Queries — as_of for point-in-time reads without a branch Guide: Atomic Transactions — compare-guarded batch writes on the parent Guide: Assert and Query Facts — basic fact writes ### Guide: Build Agent Context (guides) Guide: Build Agent Context TL;DR POST /api/context (or the MCP build_context tool) returns facts + wiki pages + a corroboration report in one call. Facts are confidence-ranked and trimmed to token_budget before the response is sent. Pass seeds to walk the graph from specific entities; pass as_of to build the packet as memory stood at a past timestamp. What is a context packet A context packet is an assembled, token-bounded bundle that contains everything an agent needs to start a task: relevant facts from the graph, adjacent wiki pages, a brief summary, a list of involved entities, and a corroboration report that flags where sources agree or conflict. It is the recommended starting point for any agentic turn. Rather than calling recall several times and stitching the results, call /api/context once. Prerequisites ABAGRAPH_BASE_URL and ABAGRAPH_API_KEY set in your environment, with at least a few facts already ingested into your tenant. Minimal working example curl -s -X POST "$ABAGRAPH_BASE_URL/api/context" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "goal": "plan the Q3 Atlas release", "seeds": ["Project:atlas"], "max_facts": 32, "token_budget": 4096 }' | jq '{summary,entities,tokens,total_tokens}' Response fields: { "kind": "context_packet", "summary": "...", "entities": ["Project:atlas", "Person:sarah", "..."], "facts": [...], "pages": [...], "tokens": 1812, "total_tokens": 3240, "collapsed": 4, "corroboration": [ { "agree": 2, "conflict": 0, "sources": ["hr-system","crm"] } ] } Request fields goal (string, required) — drives semantic fact selection; framed as a task description. seeds (string array) — entity refs to walk from (1-hop graph expansion). Stack the agent's own entity ref here so the packet always includes its persistent memory. max_facts (integer, default 32) — cap on facts returned before token trimming. token_budget (integer) — confidence-rank then trim facts to fit; omit to receive all facts up to max_facts . as_of (integer, Unix ms) — build the packet as memory stood at this timestamp; useful for replaying an agent session or debugging historical decisions. project (string) — optional project scope filter. TypeScript SDK import { createClient } from "@abagraph/client"; const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, }); const packet = await db.context({ goal: "plan the Q3 Atlas release", seeds: ["Project:atlas", "agent:reels-bot"], max_facts: 32, token_budget: 4096, }); console.log("summary:", packet.summary); console.log("facts included:", packet.facts?.length); console.log("tokens used / total:", packet["tokens"], "/", packet["total_tokens"]); Agent memory helper ( loadContext ) The @abagraph/client/agent module wraps context loading with frozen-snapshot semantics and structured logging: import { createAgentMemory } from "@abagraph/client/agent"; const mem = createAgentMemory({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, namespace: "my-tenant", agent: "reels-bot", defaultBudget: 4096, onLog: (e) = console.log(JSON.stringify(e)), }); // SESSION START — frozen snapshot; all writes go to the next session const ctx = await mem.loadContext("plan the Q3 Atlas release", { seeds: ["Project:atlas"], maxFacts: 32, }); // Pass ctx.facts to the model; ctx is immutable for this session loadContext automatically seeds agent:<name> so the packet includes the agent's own persisted state. MCP tool: build_context When abagraph is exposed over the Model Context Protocol (MCP), the build_context tool provides the same capability to any MCP-capable model: { "method": "tools/call", "params": { "name": "build_context", "arguments": { "task": "plan the Q3 Atlas release", "namespace": "my-tenant", "seeds": ["Project:atlas"], "max_facts": 32, "max_tokens": 4096 } } } The response adds a counts field ( {facts, pages, entities} ) alongside the fields returned by the REST endpoint. max_tokens caps at 100,000; the default is 4,096 tokens. Reading corroboration The corroboration array surfaces where multiple sources agree or conflict on a fact. agree > 1 means multiple sources independently corroborate that claim. Use this to calibrate how much confidence to place in each fact before passing it to the model. Time-pinned packet # Build the packet as memory stood two hours ago TWO_HOURS_AGO=$(( $(date +%s%3N) - 7200000 )) curl -s -X POST "$ABAGRAPH_BASE_URL/api/context" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"goal\":\"debug the Atlas deploy failure\",\"as_of\":$TWO_HOURS_AGO}" | jq . Verify curl -s -X POST "$ABAGRAPH_BASE_URL/api/context" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"goal":"test","max_facts":1}' | jq '.kind' # "context_packet" Guide: Persist Agent Memory — per-turn loop that calls loadContext Guide: Ingest with Digest — populate facts before building context Guide: Compact and Enrich Context — resize a packet to a tighter budget Guide: Time-Travel Queries — as_of snapshots explained ### Guide: Compact and Enrich Context (guides) Guide: Compact and Enrich Context TL;DR POST /api/compact — stateless, no write. Confidence-ranks a caller-supplied fact list and trims it to a token budget. POST /api/enrich — stateless, no write. PII-masks incoming facts and flags conflicts against currently-active facts. Both endpoints are pre-write helpers: compact before passing facts to a model; enrich before calling /api/digest or /api/transact . When to use compact Use compact when you have a larger fact list than a model's context window allows. It ranks facts by confidence and recency, then drops the lowest-ranked until the token estimate fits within the requested budget. The ranking algorithm is the same one used by the context packet and MCP build_context tool. Compact is purely stateless — it reads the list you send and returns a trimmed version. It does not read from or write to the database. Compact: minimal working example curl -s -X POST "$ABAGRAPH_BASE_URL/api/compact" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "token_budget": 500, "facts": [ {"id":"01J..","subject":"Person:sarah","predicate":"works_at","object":"Org:acme","confidence":0.9,"valid_from":1720000000000,"status":"Active","tx":1}, {"id":"01J..","subject":"Person:sarah","predicate":"role","object":"VP Engineering","confidence":0.7,"valid_from":1720000000000,"status":"Active","tx":1}, {"id":"01J..","subject":"Project:atlas","predicate":"owner","object":"Person:sarah","confidence":0.95,"valid_from":1720000000000,"status":"Active","tx":2} ] }' | jq '{kept:(.facts|length),tokens,total_tokens,dropped}' Response: { "facts": [...], "tokens": 312, "total_tokens": 480, "dropped": 0 } tokens — estimated tokens of the returned fact list. total_tokens — estimated tokens of the full input list. dropped — number of facts removed to fit the budget. Compact with the TypeScript SDK import { createClient } from "@abagraph/client"; const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, }); // Typically you receive facts from db.query() or db.context() const allFacts = await db.query({ subject: "Person:sarah" }); const trimmed = await db.compact({ facts: allFacts, token_budget: 500 }); console.log(`kept ${trimmed.facts.length}, dropped ${trimmed.dropped}`); console.log(`tokens: ${trimmed.tokens} / ${trimmed.total_tokens}`); // Pass trimmed.facts to the model prompt Compact in the agent memory helper import { createAgentMemory } from "@abagraph/client/agent"; const mem = createAgentMemory({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, namespace: "my-tenant", agent: "reels-bot", defaultBudget: 4096, }); // Load a context packet and then tighten it to a smaller window const ctx = await mem.loadContext("plan Q3 atlas release"); const tight = await mem.compact(ctx.facts ?? [], 2048); // tight.facts is now trimmed to 2048 tokens When to use enrich Use enrich as a pre-flight check before writing a batch of externally-sourced facts. It does two things without writing anything: PII masking — detects and redacts PII in every fact's object field (same mask as the digest pipeline; default is redact ). Conflict flagging — compares each incoming fact against the currently-active facts for the same subject+predicate and flags disagreements in a conflicts array. After reviewing the enrich output you can choose to assert the cleaned facts, adjust confidence, or drop flagged ones before calling /api/digest or /api/transact . Enrich: minimal working example curl -s -X POST "$ABAGRAPH_BASE_URL/api/enrich" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "facts": [ {"subject":"Person:sarah","predicate":"works_at","object":"Org:gamma","confidence":0.8}, {"subject":"Person:bob","predicate":"email","object":"bob@example.com","confidence":0.9} ], "source": "crm-sync" }' | jq . Response: { "facts": [ {"subject":"Person:sarah","predicate":"works_at","object":"Org:gamma","confidence":0.8,"source":"crm-sync"}, {"subject":"Person:bob","predicate":"email","object":"[REDACTED]","confidence":0.9,"source":"crm-sync"} ], "conflicts": [ { "subject": "Person:sarah", "predicate": "works_at", "incoming": "Org:gamma", "existing": "Org:acme", "existing_id": "01J..." } ], "masked": 1 } Bob's email is masked ( masked: 1 ). Sarah's employer conflicts with an existing Active fact. The facts array contains the cleaned inputs ready to pass to /api/digest or /api/transact if you decide to proceed. Enrich with the TypeScript SDK import { createClient } from "@abagraph/client"; const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, }); const enriched = await db.enrich({ facts: [ { subject: "Person:sarah", predicate: "works_at", object: "Org:gamma", confidence: 0.8 }, ], mask: "redact", }); if (enriched.conflicts.length 0) { console.warn("conflicts detected:", JSON.stringify(enriched.conflicts)); } // Write only after reviewing conflicts await db.digest({ facts: enriched.facts, source: "crm-sync" }); MCP: compact via build_context The MCP build_context tool runs compaction internally via its max_tokens parameter, so you rarely need to call compact separately in an MCP workflow. Set max_tokens to the model's context window and the tool returns a pre-trimmed packet: { "method": "tools/call", "params": { "name": "build_context", "arguments": { "task": "plan Q3 atlas release", "namespace": "my-tenant", "max_tokens": 2048 } } } Verify # Confirm compact returns token counts curl -s -X POST "$ABAGRAPH_BASE_URL/api/compact" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"token_budget":100,"facts":[]}' | jq '{tokens,total_tokens,dropped}' # {"tokens":0,"total_tokens":0,"dropped":0} Common mistakes Passing facts without required fields to compact. Compact expects full Fact objects (with id , status , tx , etc.), not FactInput objects. Use facts returned by query or context , not hand-crafted inputs. Expecting enrich to write anything. Enrich is a pre-flight check only; call /api/digest or /api/transact afterwards with the returned facts array. Ignoring the conflicts array. If you pass enriched facts straight to digest without reviewing conflicts, the AutoSupersede will close the existing Active facts. This may be the desired behaviour, but it should be an intentional decision. Guide: Ingest with Digest — write the enriched facts Guide: Build Agent Context — context packet includes compaction Guide: Fact-Check Contradictions — audit the graph after writing ### Guide: Connect Data Sources (guides) Guide: Connect Data Sources TL;DR GET /api/connect returns ready-to-paste REST, MCP, and stream endpoints plus copy snippets for your tenant — it never echoes your real key. Pull from a public website with POST /api/digest/scrape (BYOK scraper → digest pipeline). Push pre-extracted triples or raw text with POST /api/digest ; read live changes over GET /api/stream . What Connect does Connect is the one call that tells a tenant exactly how to reach their data. It derives the public base URL from the request Host header and returns the REST base, the MCP endpoint, the stream endpoint, and copy-paste snippets for curl, the TypeScript SDK, the CLI, an MCP client config, and the agent-memory wrapper. The caller's real token is never echoed — every snippet carries the literal <YOUR_API_KEY> placeholder. Prerequisites ABAGRAPH_BASE_URL and ABAGRAPH_API_KEY set in your environment. For scrape-based pulls: a BYOK scraper (Firecrawl or Tavily) and an OpenAI-compatible extract/judge model reachable from the server. Connector pull requires the target URL to be reachable from the managed service. Private, loopback, and cloud-metadata addresses are blocked. Step 1 — Get your endpoints curl -s "$ABAGRAPH_BASE_URL/api/connect" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq . Response (HTTP 200): { "tenant": "acme", "rest_base": "https://acme.abagraph.dev/api", "mcp_endpoint": "https://acme.abagraph.dev/mcp", "stream_endpoint": "https://acme.abagraph.dev/api/stream", "auth": "Authorization: Bearer <YOUR_API_KEY>", "snippets": { "curl": "curl -H 'Authorization: Bearer <YOUR_API_KEY>' https://acme.abagraph.dev/api/facts?subject=Person:sarah", "ts": "import { createClient } from '@abagraph/client' ...", "cli": "npx abagraph init --url https://acme.abagraph.dev --key <YOUR_API_KEY>", "mcp_config": { "mcpServers": { "abagraph": { "type": "streamable-http", "url": "https://acme.abagraph.dev/mcp", "headers": { "Authorization": "Bearer <YOUR_API_KEY>" }, "tools": ["build_context", "digest", "remember", "recall", "whats_changed"] } } } } } The tenant field reflects the namespace your key resolves to. Localhost and explicit-port hosts return http URLs; real public hosts return https . Step 2 — Pull from an external website Hand POST /api/digest/scrape a company name, domain, or URL plus your BYOK scraper and model configs. abagraph resolves the URL, fetches the public copy, runs it through the standard digest pipeline with a brand/ICP extract prompt, and returns the digest receipt with a recalled-facts preview: curl -s -X POST "$ABAGRAPH_BASE_URL/api/digest/scrape" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "company": "acme.com", "scraper": { "provider": "firecrawl", "key": "<your-scraper-key>" }, "extract_model": { "provider": "openai", "endpoint": "https://api.openai.com/v1", "key": "<your-openai-key>", "model": "gpt-4o-mini" }, "judge_model": { "provider": "openai", "endpoint": "https://api.openai.com/v1", "key": "<your-openai-key>", "model": "gpt-4o-mini" }, "threshold": 0.7, "mask": "redact" }' | jq . Response — the digest report plus the resolved brand entity, a display label , and a recalled preview of the facts just written: { "written": 6, "superseded": 0, "candidates": 1, "pii_masked": 2, "brand": "Brand:acme", "label": "Acme Corp", "recalled": [ { "subject": "Brand:acme", "predicate": "sells", "object": "developer tooling", "confidence": 0.88 } ] } Scraper and model keys live only for their outbound calls; only redacted configs are logged. Re-scrapes map to the same Brand:<slug> entity because the slug is derived from the stable resolved host, not the volatile page title. Step 2b — Register a connector Register a named connector to pull structured data from an external endpoint. abagraph maps the response fields to subject/predicate/object triples using the mapping you provide: curl -s -X POST "$ABAGRAPH_BASE_URL/api/connectors" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "crm", "kind": "webhook", "label": "CRM Contacts", "url": "https://crm.example.com/api/contacts", "mapping": { "subject": "Contact:$id", "fields": { "company": "works_at", "role": "role", "email": "email" } } }' | jq . Step 2c — Ingest via pull Tell abagraph to fetch the connector's URL and digest the response. The managed service keeps a slow source from blocking other writes: curl -s -X POST "$ABAGRAPH_BASE_URL/api/connectors/crm/pull" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq . List and inspect connectors # List all connectors (shows ingested fact count per connector) curl -s "$ABAGRAPH_BASE_URL/api/connectors" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq . # Inspect one connector and see its 20 most-recent facts curl -s "$ABAGRAPH_BASE_URL/api/connectors/crm" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '{name,ingested,recent:[.recent[]|{subject,predicate,status}]}' Automated polling Set the environment variable ABAGRAPH_CONNECTOR_POLL_SECS to a positive integer to have abagraph poll all pull-mode connectors on that interval. The default is 0 (polling disabled): pulls must be triggered explicitly or via an external scheduler. On a dedicated deployment, set this variable in the service configuration to enable continuous polling without an external cron job. Step 3 — Push facts from your own systems For data you already hold (CRM exports, event streams, internal docs), push pre-extracted triples straight to POST /api/digest in facts mode — no model required. Supersede and PII masking still run: curl -s -X POST "$ABAGRAPH_BASE_URL/api/digest" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "facts": [ {"subject":"Person:sarah","predicate":"works_at","object":"Org:acme","confidence":0.9}, {"subject":"Project:atlas","predicate":"owner","object":"Person:sarah","confidence":0.95} ], "source": "crm-export" }' | jq '{written,superseded,candidates}' Run this on a schedule or from a webhook handler to keep the graph current as your source systems change. Step 4 — Read live changes The stream endpoint from Connect emits server-sent events as facts land, so a downstream consumer can react without polling: curl -N "$ABAGRAPH_BASE_URL/api/stream" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" Subscribe before triggering a connector pull to watch facts arrive in real time: # Terminal 1: subscribe curl -s -N "$ABAGRAPH_BASE_URL/api/stream" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" # Terminal 2: trigger pull curl -s -X POST "$ABAGRAPH_BASE_URL/api/connectors/crm/pull" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" TypeScript SDK import { createClient } from "@abagraph/client"; const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, }); // Scrape a public site into the graph const receipt = await db.digest.scrape({ company: "acme.com", scraper: { provider: "firecrawl", key: process.env.FIRECRAWL_KEY! }, }); // Push facts from your own system await db.digest({ facts: [ { subject: "Person:sarah", predicate: "works_at", object: "Org:acme", confidence: 0.9 }, ], source: "crm-export", }); console.log("brand:", receipt.brand, "written:", receipt.written); Verify curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Brand:acme" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '[.[] | {predicate,object,confidence}]' Common mistakes Expecting Connect to return your real key. Every snippet uses the literal <YOUR_API_KEY> placeholder — substitute your own key before running them. Calling scrape without a scraper config. POST /api/digest/scrape needs a company , domain , or url plus a scraper block; an empty identifier returns 400 SchemaInvalid . Assuming every scraped fact is Active. Facts below threshold (default 0.7 ) land as Candidate — counted under candidates , not written . Re-scraping to a different brand. The Brand:<slug> entity is keyed on the resolved host, so re-scrapes and provider swaps reconcile to the same entity rather than forking. Connector name collision. Names are unique per tenant. Registering the same name twice returns the existing connector silently — it does not create a second one. Pull against a private or loopback URL. The managed service SSRF-guards all fetch requests and blocks private, loopback, and cloud-metadata addresses. Expecting polling without setting the env var. ABAGRAPH_CONNECTOR_POLL_SECS=0 (the default) disables polling. Set it to a positive integer to enable. Guide: Ingest with Digest — the digest pipeline in depth (raw content + facts modes) Guide: Build Agent Context — consume the ingested facts in a turn Guide: Assert and Query Facts — single-fact writes without extraction Guide: Fact-Check Contradictions — surface conflicts as sources disagree ### Guide: Fact-Check Contradictions (guides) Guide: Fact-Check Contradictions TL;DR GET /api/factcheck is read-only and tenant-scoped — it never writes. Three contradiction kinds: multiple_active , expired_but_active , candidate_conflict . Set-valued predicates ( tag , references , cites , knows , member_of ) are exempt by default; add more with ?coexist= . When to use fact-check Use /api/factcheck to audit graph health after bulk ingestion, to surface conflicts before building agent context, or as a scheduled integrity check in production. It does not repair anything — it reports. Contradiction kinds multiple_active — more than one Active fact for the same subject+predicate on a functional (non-coexist) predicate. This indicates a supersede that was not applied, or two concurrent writes that should have raced but both landed Active. expired_but_active — a fact whose valid_until has passed but whose status is still Active (the TTL sweeper has not run yet, or transient was not set). candidate_conflict — a Candidate fact disagrees with an Active fact for the same subject+predicate. Reasserting at full confidence would supersede the Active fact; this flag surfaces the tension so you can decide. Minimal working example curl -s "$ABAGRAPH_BASE_URL/api/factcheck" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq . Example response: { "contradictions": [ { "subject": "Person:sarah", "predicate": "works_at", "kind": "multiple_active", "facts": [ {"id":"01J...", "object":"Org:acme", "confidence":0.9, "status":"active", "source":"hr"}, {"id":"01J...", "object":"Org:beta", "confidence":0.8, "status":"active", "source":"crm"} ] } ], "count": 1 } Filter by subject or predicate # All contradictions for one entity curl -s "$ABAGRAPH_BASE_URL/api/factcheck?subject=Person:sarah" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq . # Contradictions on a specific predicate across all entities curl -s "$ABAGRAPH_BASE_URL/api/factcheck?predicate=works_at" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq . Coexist override The following predicates are exempt from multiple_active detection by default because they are intentionally set-valued: references , tag , knows , member_of , cites . Add your own set-valued predicates via the coexist query parameter (comma-separated): curl -s "$ABAGRAPH_BASE_URL/api/factcheck?coexist=label,alias,skill" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq . TypeScript SDK import { createClient } from "@abagraph/client"; const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, }); const result = await db.factcheck({ subject: "Person:sarah" }); console.log(`${result.count} contradiction(s) found`); for (const c of result.contradictions) { console.log(`${c.kind} — ${c.subject} ${c.predicate}`); for (const f of c.facts) { console.log(` ${f.object} (confidence=${f.confidence}, source=${f.source})`); } } Resolving contradictions Fact-check reports but does not fix. To resolve: multiple_active — retract the stale fact with DELETE /api/facts/:id , or reassert the authoritative value (AutoSupersede will close the others). expired_but_active — retract the expired fact, or let the TTL sweeper handle it if the fact was written with transient: true and a valid_until . candidate_conflict — reassert the winning value at full confidence (≥ threshold) to promote and supersede, or retract the candidate to clear the conflict. Verify (clean state) curl -s "$ABAGRAPH_BASE_URL/api/factcheck" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '.count' # 0 Common mistakes Expecting factcheck to fix anything. It is read-only. Use retract or reassert to repair. Forgetting to add custom set-valued predicates to coexist . Without the override, two active alias facts for the same entity will be flagged as multiple_active . Running factcheck on a large graph without subject/predicate filters. A full scan over all Active + Candidate facts can be slow on large stores. Filter early. Guide: Assert and Query Facts — understand Active vs Candidate status Guide: Compact and Enrich Context — enrich flags conflicts before writing Guide: Atomic Transactions — prevent races that create multiple_active ### Guide: Ingest with Digest (guides) Guide: Ingest with Digest TL;DR POST /api/digest has two modes: raw content (load → chunk → BYOK extract → judge → assert) or a pre-extracted facts array. PII is masked before any fact touches storage or the embedding model. Facts below the judge's confidence threshold land as Candidate — stored but not superseding. Two modes Choose based on whether you have raw text or already-structured triples: Raw content mode — pass content + content_type . abagraph chunks the text, calls your BYOK extractor model, runs a judge, then asserts passing facts. Facts mode — pass facts , a pre-extracted array of triples. PII masking and supersede logic still run; extraction and judging are skipped. Prerequisites ABAGRAPH_BASE_URL and ABAGRAPH_API_KEY set in your environment. For raw content mode: a BYOK extraction model (any OpenAI-compatible endpoint) reachable from the abagraph service. Facts mode — minimal working example The simplest path: send pre-extracted triples. No model required. curl -s -X POST "$ABAGRAPH_BASE_URL/api/digest" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "facts": [ {"subject":"Person:sarah","predicate":"works_at","object":"Org:acme","confidence":0.9}, {"subject":"Person:sarah","predicate":"role","object":"VP Engineering","confidence":0.85}, {"subject":"Project:atlas","predicate":"owner","object":"Person:sarah","confidence":0.95} ], "source": "crm-export" }' | jq . Response: { "written": 3, "written_ids": ["01J...", "01J...", "01J..."], "superseded": 0, "candidates": 0, "entities": ["Person:sarah", "Project:atlas"], "pii_masked": 0 } Raw content mode Pass content , a content_type (e.g. text/markdown , text/plain ), and BYOK model configs. The extract_model and judge_model fields accept any OpenAI-compatible config: curl -s -X POST "$ABAGRAPH_BASE_URL/api/digest" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "content": "Sarah joined Acme Corp in January 2024 as VP Engineering. She leads the Atlas project.", "content_type": "text/plain", "source": "hr-doc-2024", "extract_model": { "provider": "openai", "endpoint": "https://api.openai.com/v1", "key": "<your-openai-key>", "model": "gpt-4o-mini" }, "judge_model": { "provider": "openai", "endpoint": "https://api.openai.com/v1", "key": "<your-openai-key>", "model": "gpt-4o-mini" }, "threshold": 0.7, "mask": "redact" }' | jq . PII masking The mask field controls how detected PII is handled before storage and embedding: "redact" (default) — replaces PII with [REDACTED] . "hash" or "hash_sha256" — replaces PII with its SHA-256 hash (consistent, reversible offline). "shift_date" — shifts detected dates by ±365 days. PII masking runs before the extractor calls out to your model and before any value reaches the embedding model or storage. Candidate facts Facts where the judge's confidence score is below threshold (default 0.7 ) land as Candidate . They are stored and searchable but do not supersede an existing Active fact. Reassert at full confidence to promote a Candidate to Active. TypeScript SDK Facts mode with the SDK ( @abagraph/client ): import { createClient } from "@abagraph/client"; const db = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, }); const result = await db.digest({ facts: [ { subject: "Person:sarah", predicate: "works_at", object: "Org:acme", confidence: 0.9 }, { subject: "Project:atlas", predicate: "owner", object: "Person:sarah", confidence: 0.95 }, ], source: "crm-export", }); console.log("written:", result.written, "superseded:", result.superseded); Raw content mode: const result = await db.digest({ content: "Sarah joined Acme Corp in January 2024 as VP Engineering.", content_type: "text/plain", source: "hr-doc", }); console.log("written:", result.written); Agent memory digest helper The createAgentMemory wrapper exposes a fire-and-forget digest method for use during or after an agent session: import { createAgentMemory } from "@abagraph/client/agent"; const mem = createAgentMemory({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, namespace: "my-tenant", agent: "reels-bot", }); // Fire-and-forget at session end mem.digest({ content: transcript, source: "session-2024-07-01" }).catch(() = undefined); Verify curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:sarah" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '[.[] | {predicate,object,status}]' Common mistakes Sending both content and facts in the same request. The server checks for facts first; if present, it enters facts mode and ignores content . Use one mode per request. Empty content in raw mode. The server returns 400 SchemaInvalid : "content is required". Expecting extraction without a model config. In raw content mode, extract_model is required. If omitted, the call returns a schema error. Assuming all facts are written immediately. Facts below threshold are written as Candidate , reflected in the candidates count in the response, not in written . Guide: Build Agent Context — consume the ingested facts Guide: Compact and Enrich Context — pre-flight check before writing Guide: Connect Data Sources — continuous ingestion from external systems Guide: Assert and Query Facts — single-fact writes without extraction ### Guide: Persist Agent Memory (guides) Guide: Persist Agent Memory TL;DR Call loadContext(task) at session start to get a frozen snapshot of relevant facts. Call remember(facts) during or after the turn to persist new knowledge. New writes are NOT visible in the current session ( inContext: false ) — they appear in the next loadContext call. For runner-level wiring, install memoryPlugin once; every agent gets memory automatically. Architecture of a memory-enabled agent turn An agent turn with abagraph has three phases: Load ( loadContext ) — assemble a frozen, token-bounded snapshot of relevant facts and wiki pages from the graph. The snapshot does not change during the turn. Run — execute the model with the snapshot in context. The model may emit new facts to persist. Remember ( remember ) — write the new facts back to abagraph. They become available to future turns and other agents in the same namespace. Prerequisites abagraph server running: cargo run --bin abagraph-server . Node.js >= 18. Install the SDK: npm install @abagraph/client . Per-turn loop with createAgentMemory import { createAgentMemory } from "@abagraph/client/agent"; const mem = createAgentMemory({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, namespace: "my-tenant", // isolation scope; matches your tenant token agent: "reels-bot", // used to seed the entity "agent:reels-bot" in every loadContext source: "agent:reels-bot", // provenance tag on every remember write defaultBudget: 4096, // token budget for loadContext onLog: (e) = console.log(JSON.stringify(e)), // optional structured log per operation }); // SESSION START const ctx = await mem.loadContext("plan the Q3 Atlas release", { seeds: ["Project:atlas"], // extra entity refs to walk from maxFacts: 32, }); // ctx.facts is the frozen snapshot for this session console.log("relevant facts:", ctx.facts?.length); // ... run model with ctx.facts as context ... const modelOutput = { decisions: ["ship on 2024-09-01", "cut scope by 20%"] }; // REMEMBER: persist structured facts back await mem.remember([ { subject: "Project:atlas", predicate: "target_ship_date", object: "2024-09-01", confidence: 0.9, reason: "decided in Q3 planning session", // folded into properties.reason }, { subject: "Project:atlas", predicate: "scope_cut", object: "20%", confidence: 0.85, why: "budget constraints", // folded into properties.why }, ]); // {persisted: true, inContext: false, ids: ["01J...", "01J..."]} Convenience: turn() helper The turn method combines load, run, and remember into a single call: const result = await mem.turn( "plan the Q3 Atlas release", async (ctx) = { // ctx is the frozen context packet const answer = await myModel(ctx.facts); return { result: answer, facts: [ { subject: "Project:atlas", predicate: "plan_summary", object: answer.summary, confidence: 0.9 }, ], }; }, ); Runner-level wiring with memoryPlugin Install once at the runner; every agent session loads and persists memory automatically: import { memoryPlugin } from "@abagraph/client/plugin"; const plugin = memoryPlugin({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY, namespace: "my-tenant", agent: "reels-bot", }); // SESSION START: call this before running the agent const ctx = await plugin.onSessionStart({ task: "plan the Q3 Atlas release" }); // ctx is the frozen context packet; pass ctx.facts to the model // ... run agent ... // SESSION END: fire-and-forget persist plugin.onSessionEnd({ content: transcript, // OR // facts: [...] // pre-extracted facts if content is not available }); onSessionEnd returns immediately. The digest call is fire-and-forget — the caller continues without waiting for the write to complete. If the write fails it is silently swallowed so the session does not crash at teardown. Feedback fields in remember RememberInput accepts three optional feedback fields that are folded into the fact's properties object before the write: reason — why this fact was asserted. why — alternative provenance note (alias for reason). how — how the conclusion was reached. await mem.remember([{ subject: "Project:atlas", predicate: "risk_level", object: "high", reason: "dependency on third-party API that has not yet stabilised", how: "inferred from changelog analysis", confidence: 0.75, }]); Rust agent kernel For a durable Rust-based agentic loop, use run_kernel directly. The kernel assembles a TurnContextPacket per turn, calls your ModelProvider , persists each step as one transact() batch, and mirrors step content to the Fact store for kNN recall. The loop is resumable — re-enter run_kernel with the same run_id to pick up from the last step: // See examples/agent_run.rs for a complete working example // cargo run --example agent_run use abagraph::kernel::{run_kernel, SpecInput, RunOpenOpts}; use abagraph::kernel::persist::{open_spec, open_run}; use abagraph::datom_graph::{DatomGraph, register_agent_schemas}; use abagraph::{Abagraph, OpenOptions}; use std::sync::Mutex; let g = DatomGraph::open("agent.redb")?; let aba = Mutex::new(Abagraph::open(OpenOptions::new())); register_agent_schemas(&g)?; let spec_id = open_spec(&g, SpecInput { role: "researcher".into(), goal: "summarise project atlas".into(), model: "gpt-4o".into(), autonomy_policy: "full".into(), tool_scope: Vec::new(), context_source: Vec::new(), success_criteria: "atlas modules".into(), })?; let run_id = open_run(&g, spec_id, RunOpenOpts { trigger: "manual".into(), tenant: None, budget_max_steps: Some(8), budget_max_tokens: Some(2_000), })?; let outcome = run_kernel(&aba, &g, &summarizer, &model, &tools, Some(&verifier), run_id)?; println!("{:?}", outcome); // RunOutcome: Success | Failure | Budget | AwaitingApproval Verify # Confirm the remembered fact is visible in the next session curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Project:atlas&predicate=target_ship_date" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '.[0].object' # "2024-09-01" Common mistakes Expecting remember to update the current session context. remember always returns inContext: false . New facts appear only in the next loadContext call. Awaiting onSessionEnd . It is void and fire-and-forget by design. Awaiting it will throw a TypeScript type error. Using different namespace values for load and remember. loadContext seeds facts scoped to the namespace; remember writes to the same namespace. A mismatch means writes land in a different tenant and are invisible to future loadContext calls. Scaling the Rust kernel beyond one replica. The kernel uses a single-writer engine ( src/transactor/mod.rs ). Never run more than one replica pointing at the same storage. Guide: Build Agent Context — loadContext internals and options Guide: Ingest with Digest — bulk ingest for session end Guide: Compact and Enrich Context — resize context before passing to model examples/agent_run.rs — runnable Rust kernel demo ### Guide: Time-Travel Queries (guides) Guide: Time-Travel Queries TL;DR Every write is bitemporal: valid_from / valid_until captures world-time; tx captures write-time. Add ?as_of=<unix-ms> to any read endpoint to see the world as it stood at that instant, including now-superseded facts. Combine ?as_of= with ?status=Superseded to audit the full history of a predicate without pinning an exact timestamp. How bitemporal storage works abagraph records two time dimensions for every fact: Validity interval ( valid_from , valid_until ) — the interval during which the fact was true in the real world. An open valid_until (null) means "still true". Transaction time ( tx , recorded_at ) — when the fact was written into the database. Each committed write (assert, retract, transact) increments the global tx counter and stamps it on every fact the write touches. When you supersede a fact (assert a conflicting value), the engine atomically sets valid_until on the old fact and writes the new one. Both are kept. as_of(t) returns the set of facts whose validity interval contained t , including facts that are now Superseded. Time-travel over REST Append ?as_of=<unix-ms> to /api/facts or /api/search : # 1. Assert a fact now curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme"}' | jq '.valid_from' # e.g. 1720000001000 # 2. Record "now" and wait a moment, then supersede T_BETWEEN=$(date +%s%3N) sleep 1 curl -s -X POST "$ABAGRAPH_BASE_URL/api/facts" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:beta"}' | jq . # 3. Present: only Beta is Active curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:sarah&predicate=works_at" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '.[].object' # "Org:beta" # 4. Past: Acme was active at T_BETWEEN curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:sarah&predicate=works_at&as_of=$T_BETWEEN" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '.[].object' # "Org:acme" Query superseded facts Add ?status=Superseded to see the full history of a predicate without pinning a timestamp: curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:sarah&predicate=works_at&status=Superseded" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '[.[] | {object,valid_from,valid_until}]' MCP: what changed between two timestamps The MCP whats_changed tool surfaces the diff between two points in time for a namespace: { "method": "tools/call", "params": { "name": "whats_changed", "arguments": { "from": 1720000000000, "to": 1720003600000, "namespace": "my-tenant", "predicate": "works_at" } } } Response fields: from , to , added , superseded , still_valid . Time-pinned context packet Pass as_of to POST /api/context to build the agent's context packet as memory stood at any past time — useful for replaying a past decision or auditing an agent session: curl -s -X POST "$ABAGRAPH_BASE_URL/api/context" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"goal\":\"audit the atlas deploy decision\",\"as_of\":$T_BETWEEN}" | jq . Transaction cursor for replay The tx field on every fact is a monotonic write-transaction number. Use it as a cursor to replay writes from a known checkpoint: # Fetch all facts written after tx 42 curl -s "$ABAGRAPH_BASE_URL/api/facts?tx_gt=42" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq '[.[] | {subject,predicate,tx}]' Verify # Confirm the past snapshot and the present differ PAST=$(curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:sarah&predicate=works_at&as_of=$T_BETWEEN" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq -r '.[0].object') PRESENT=$(curl -s "$ABAGRAPH_BASE_URL/api/facts?subject=Person:sarah&predicate=works_at" \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" | jq -r '.[0].object') echo "past=$PAST present=$PRESENT" Common mistakes Passing Unix seconds instead of Unix milliseconds. abagraph timestamps are milliseconds. A seconds-precision timestamp will return an empty result or an unexpectedly old snapshot. Expecting as_of to show Retracted facts. Retracted facts are not bitemporal snapshots — they are explicitly hidden. Use ?status=Retracted to see them. Confusing world-time validity with write-time cursor. as_of travels world-time ( valid_from / valid_until ); tx_gt replays write-time ( tx ). They address different questions. Guide: Assert a Fact — triple model and supersede mechanics Guide: Branch and Merge — branches also pin an as_of base_ts Guide: Build Agent Context — time-pinned context packets ### Conflict & Supersession (layers) Conflict & Supersession TL;DR Use this page when two writes disagree about the same thing and you need to know which one wins, what happens to the loser, and how to keep both when that is correct. The outcome: assert a new value and the old one closes itself in the same transaction — a functional predicate never has two competing active values, and the superseded value stays recoverable via time-travel. Functional predicates (the default): the prior active fact is closed ( valid_until set, status: Superseded ) and the new one opened — one atomic batch. Set-valued predicates (e.g. references , tag ) opt out per assert with the coexist policy, so many values live side by side. Confidence below the threshold (default 0.75 ) lands as Candidate and does not supersede; reasserting at full confidence promotes it. What it does This layer decides what a new assert does to the facts already there. The default — and the reason agent memory stays consistent — is that a functional predicate (one active object per subject, like works_at ) auto-supersedes: writing a new value closes the old one and opens the new one atomically, so a reader never catches the graph mid-change with two active answers. Use this page when a value is being corrected or updated, when you want to keep multiple values for one predicate, or when a low-trust claim should be staged rather than promoted. Closing is not deletion. The superseded fact keeps its history and reappears under as_of — so "what did we believe before the correction?" still has an answer. A tiny data example Sarah moves from Globex to Acme. The new assert closes the old fact and opens the new one in a single transaction (note the shared tx ): curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme","confidence":0.95,"source":"hr-system"}' \ 'https://nas.abagraph.com/api/facts' // before — one active fact {"subject":"Person:sarah","predicate":"works_at","object":"Org:globex","valid_until":null,"status":"Active","tx":40} // after the assert — old closed, new opened, atomically {"subject":"Person:sarah","predicate":"works_at","object":"Org:globex","valid_until":1718600000.0,"status":"Superseded","tx":58} {"subject":"Person:sarah","predicate":"works_at","object":"Org:acme","valid_until":null,"status":"Active","tx":58} How to customize it Knob Effect Default policy: "auto_supersede" Close the conflicting active fact, open the new one. Why: keeps functional predicates single-valued. this (per assert) policy: "coexist" Allow many active facts for the same subject + predicate. Why: set-valued predicates ( references , tag ) legitimately hold multiple values. opt-in policy: "reject" Refuse the write if a conflicting active fact exists; returns a Conflict error. Why: enforce write-once invariants. opt-in confidence vs threshold confidence < threshold → Candidate : stored and searchable but it does not supersede anything. Reasserting at ≥ threshold promotes it to Active . Why: low-trust claims should not overwrite trusted ones. threshold 0.75 Coexist is a per-assert choice, not a global schema setting — the same predicate can supersede in one write and coexist in another. An exact duplicate (same object, same source, confidence within 0.001) is a no-op and returns the existing fact instead of writing a second copy. The policy string is case-insensitive. See /docs#api/facts for the request shape. What it logs Supersession is silent in the process log by design — the closed fact and the new fact are the record. You observe a conflict decision two ways: ?explain=1 on POST /api/facts returns an envelope whose stats spell out the decision: policy , active_before , active_after , active_delta , and the new fact's resulting status ( active or candidate ). A clean supersede shows active_before: 1 , active_after: 1 , active_delta: 0 . The reject policy returns {"error":{"code":"Conflict","message":"…"}} (HTTP 409) listing the existing fact ids — the only case where a conflict surfaces as an error rather than a quiet rewrite. How it's isolated Owns: the decision of what an assert does to prior facts, and the planning of the resulting atomic batch (close N conflicting facts, open the new one). It sets valid_until and Superseded on the closed facts and nothing else on them. Hands off to: the Storage layer , which commits that batch all-or-nothing. It reads candidate active facts through storage's lookups but never writes bytes itself. Never touches: cross-tenant data — conflict resolution is namespace-local, so an assert in one tenant never supersedes another tenant's fact even on a shared subject + predicate (the global scope is its own namespace). It also never invents new timestamps beyond the close, and never picks query indexes. Status Stable. Shipped today: per-assert auto_supersede / coexist / reject policies, candidate-threshold staging and promotion, idempotent-duplicate suppression, and tenant-local resolution — all over the REST and MCP surfaces. Source-available components are on the roadmap. Facts & Bitemporal Time — status lifecycle and confidence Storage Layer — how the atomic batch commits API: /api/facts — the policy field and error codes ### Context Assembly (layers) Context Assembly TL;DR Use this page when you are about to start an agent turn and need "everything relevant to this goal, small enough to fit the prompt" in one call — not a pile of raw facts you have to trim yourself. build_context (MCP) / POST /api/context returns one packet : seed entities, deduplicated and confidence-ranked facts, related wiki pages, a corroboration signal, and a rendered system_prompt . It is token-budgeted : set token_budget and the highest-confidence, most-recent facts are kept until the budget is full; the rest are dropped (or folded into one derived summary). It is deterministic and offline by default — no model call unless you opt into BYOK tail-compaction with compact_model . Raw embeddings are stripped from the response; you get readable triples with provenance, never float noise. What it does Use this layer at the start of an agent turn . Instead of you running several searches, walks, and page lookups and then guessing what to keep, one call assembles a single compact packet for a stated goal . A context packet is a bounded bundle: the facts that matter, ranked so the most trustworthy come first, trimmed to a token budget, plus a system prompt you can paste straight into the model. The pipeline, in order: resolve seed entities from the goal/seeds → look up their facts via the indexes → compact (drop exact duplicates, compute corroboration, optionally cluster near-duplicates, then rank by confidence and recency and trim to the token budget) → optionally fold the dropped tail into one derived fact (BYOK) → add a 1-hop graph walk and related wiki pages → render a system_prompt in the requested style. Corroboration = how many independent facts agree on the same claim; a higher agreement count is a stronger trust signal than a single high-confidence fact. A tiny data example Ask for context on a goal, capped to a token budget: curl -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"goal":"prep for the Acme renewal call","seeds":["Org:acme"],"token_budget":1500,"style":"claude"}' \ 'https://api.abagraph.com/api/context' { "kind": "context_packet", "summary": "Context for: prep for the Acme renewal call", "entities": ["Org:acme", "Person:sarah"], "facts": [ { "subject": "Person:sarah", "predicate": "works_at", "object": "Org:acme", "confidence": 0.95, "source": "hr-system", "status": "Active" }, { "subject": "Person:sarah", "predicate": "raised_concern", "object": "price is too high", "confidence": 0.9, "source": "call-2026-06-20", "status": "Active" } ], "pages": [], "tokens": 142, "total_tokens": 880, "collapsed": 3, "corroboration": [ { "agree": 2 }, { "agree": 1 } ], "system_prompt": " \n Person:sarah works_at Org:acme ...\n " } Read it as: collapsed: 3 duplicate facts were merged; the budget cut the candidate set from total_tokens 880 down to tokens 142 ; the first fact is corroborated by 2 independent sources. How to customize it Field Default Effect goal — Free-text mission; drives seed resolution and the rendered prompt. seeds [] Explicit start entities ( Type:id ) to anchor the packet on. max_facts 64 Hard ceiling on facts when no token_budget is set. max_pages 8 Ceiling on attached wiki pages. token_budget unset When set, switch from a count cap to confidence/recency ranking trimmed to fit this many tokens. Why: a token budget maps directly to model context limits. cluster_threshold 0.0 (off) Cosine threshold for collapsing near-duplicate facts after exact dedup; a typical value is 0.97 . Needs an embedding provider. compact_model unset BYOK model config: fold the budget-dropped tail into one derived digest fact. Unset keeps the path fully offline and deterministic. as_of now Unix-ms: assemble the packet from the world as it was known then. style generic System-prompt format: claude (XML), openai (markdown), else plain. Presentation only. role generic Agent role/mission woven into the rendered prompt. Presentation only. include_recent / include_conflicts true / true Whether to fold in latest activity and surface conflicting claims. Near-duplicate clustering and tail-compaction depend on an embedding provider — see Configuration: Embedding Providers . What it logs Context assembly is read-only and deterministic, so it does not emit a dedicated process-log line per call. Its work is surfaced inline via the ?explain=1 query-plan envelope, which every endpoint supports: Envelope key Meaning op "context" — the operation. plan The pipeline: parse request → seed entities → indexed fact lookup → 1-hop graph walk → wiki page lookup. stats entities , facts , pages , collapsed (duplicates merged), corroborated (facts with >1 agreeing source). total_ms Wall-clock latency for the call. If you opt into compact_model , that model call is governed by your BYOK provider and logged on its own surface. The boot-time abagraph.server.start line confirms the process is up. How it's isolated This layer owns fusion and shaping : take facts (and pages and walk results) that other layers produce, and turn them into one ranked, budgeted, presentation-ready packet. It is strictly tenant-scoped — for a non-admin caller the request's tenant is forced onto the query, so a packet only ever contains the caller's own memory. It hands fact storage to the fact layer, semantic similarity to the Vectors layer (used only when clustering is enabled), and relationship expansion to the Graph & Walk layer. It deliberately never writes facts , never calls a model unless compact_model is set, and never leaks embeddings — the vector fields are stripped from facts and derived_facts before the packet is returned. Status Stable. Shipped today: POST /api/context and the build_context MCP tool returning seed entities, deduplicated and confidence/recency-ranked facts, corroboration counts, 1-hop graph walk, related wiki pages, token-budget trimming, and a rendered system_prompt ( claude / openai /generic). Optional, opt-in: near-duplicate clustering ( cluster_threshold ) and BYOK tail-compaction ( compact_model ) — the default path is deterministic and offline. Related API Reference: /api/context Concept: Context Packets Agents: Per-Turn Memory Loop — load a packet, reason, persist Layer: Vectors & Semantic Search Layer: Graph & Walk ### Facts & Bitemporal Time (layers) Facts & Bitemporal Time TL;DR Use this page when you need to record a claim that changes over time, or answer "what did we believe last Tuesday?" without bolting on a separate audit log. The outcome: every fact carries two clocks, so the engine can reconstruct both what was true in the world and what the system knew — at any past instant. A fact is a triple {subject, predicate, object} plus provenance: valid_from , valid_until , recorded_at , confidence , source , status , and a monotonic tx . Valid time = when the claim is true in the world ( valid_from → valid_until ). Record time = when the engine learned it ( recorded_at , always "now" at write). Confidence ≥ threshold (default 0.75 ) → Active ; below it → Candidate (stored and searchable, but does not win conflicts). What it does This layer defines what a fact is and gives it two independent time axes. That is the whole payoff: you can ask "who did Sarah work for on 1 March?" (valid time) separately from "what did we know on 1 March?" (record time) — and the answer to the second never changes, because the past is append-only. There is no separate audit table to keep in sync. Use this page when a value will change and you must keep the history: employment, status, scores, beliefs an agent revises. Define the two clocks once and time-travel comes for free. Valid time — valid_from / valid_until : the window during which the claim holds in the real world. valid_until: null means open-ended (still true). You set these; backdating is allowed. Record time — recorded_at : the instant the engine learned the fact. Always the write clock, never backdated. This is the axis an audit answers on. tx — a monotonic write number stamped on every fact a single write touches, so consumers can replay only what is new with ?tx_gt= . A tiny data example Sarah joined Acme on 1 June 2024; the engine learned it a second later from the HR system: { "subject": "Person:sarah", "predicate": "works_at", "object": "Org:acme", "valid_from": 1717200000.0, "valid_until": null, "recorded_at": 1717200001.2, "confidence": 0.95, "source": "hr-system", "status": "Active", "tx": 42 } Read it back as it stood at any instant by adding a time-travel parameter — the engine returns the world-state visible at that record time, Superseded facts included: curl -H "Authorization: Bearer $TOKEN" \ 'https://nas.abagraph.com/api/facts?subject=Person:sarah&as_of=1717200000' How to customize it Field / knob Effect Default confidence 0.0–1.0 trust score. Why: below the threshold a fact lands as Candidate and will not supersede. 1.0 candidate threshold (deployment) The cutoff: confidence < threshold → Candidate , else Active . Why: tune how much trust a fact needs to count. 0.75 valid_from / valid_until The validity window. Why: backdate a known-earlier truth, or close one with a future date. now / null (open) source Provenance label (URL, agent name, document id). Why: every recall can show where a claim came from. none properties Free-form JSON annotations (the why/how, reasoning traces). Why: attach context without inventing predicates. none transient With a valid_until , the fact is hard-deleted after expiry. Why: ephemeral scratch memory instead of permanent history. false Object values are typed — string, number, boolean, or null; a string object is treated as a walkable entity reference. Per-assert conflict behaviour is covered in the Conflict layer . What it logs One JSON line per event to the process log. A normal assert is silent — the fact itself, with its two timestamps and tx , is the durable record. The one write-path event this layer surfaces protects provenance from leaking secrets: Event Level Fields Meaning abagraph.pii.masked info path , masked Detected PII was redacted from a fact before it reached storage or the embedder; masked counts the redactions. Time-travel reads ( as_of ) emit nothing — they are pure reads over history. How it's isolated Owns: the fact shape, the two time axes, the confidence / source / properties provenance, and the status lifecycle ( Active , Candidate , Superseded , Retracted ). It decides a new fact's initial status from its confidence. Hands off to: the Storage layer for durability (it never writes bytes itself) and the Conflict layer for the decision of what an assert does to prior facts. Never touches: index selection, graph walks, or network I/O. It mints a fact and stamps its timestamps; it does not choose how that fact is later found. Status Stable. Shipped today: typed triples, both time axes, confidence-driven status, provenance fields, transient TTL facts, and as_of time-travel reads over the full REST surface. Source-available components are on the roadmap. Storage Layer — how facts are persisted Conflict & Supersession — what a contradicting assert does Facts and Triples — every field in detail ### Graph & Walk (layers) Graph & Walk TL;DR Use this page when you want to answer "what is connected to X, and how?" — e.g. "everyone at Sarah's company and what they work on" — without writing recursive queries. Any fact whose object is an entity reference (a Type:id string) is a typed edge : Person:sarah --works_at--> Org:acme is one edge named works_at . Walk = breadth-first traversal (BFS): start at one entity, expand neighbours level by level. GET /api/walk?start=… . Filters: direction ( out / in / both ), edge_types (comma-separated predicate allow-list), depth (hops, default 2), and as_of (walk the graph as it was at a past time). A walk never crosses tenant boundaries and never follows retracted or non-entity edges. What it does Use this layer when the question is about relationships : reachability, neighbourhoods, and paths. Two definitions for juniors: Typed edge — a fact whose object names another entity. The predicate is the edge type. A fact with a literal object (a number, a free-text note) is not an edge and walk skips it. Breadth-first walk (BFS) — start from one node, visit all its direct neighbours (depth 1), then their neighbours (depth 2), and so on up to depth . Each node is visited once; each edge is emitted once. Two endpoints sit on this layer: GET /api/walk returns the reached nodes plus the edges traversed from a starting entity; GET /api/graph returns a node/edge view of the whole tenant graph (or a ?q= -focused neighbourhood) for visualisation. A tiny data example Given two facts: Person:sarah --works_at--> Org:acme valid_from 2026-01-01, confidence 0.95, source hr-system Person:omar --works_at--> Org:acme valid_from 2026-03-01, confidence 0.90, source hr-system Walk into Acme to find its people — depth 1, incoming edges, only the works_at edge type: curl -H "Authorization: Bearer $TOKEN" \ 'https://api.abagraph.com/api/walk?start=Org:acme&direction=in&depth=1&edge_types=works_at' { "start": "Org:acme", "reached": ["Org:acme", "Person:sarah", "Person:omar"], "edges": [ { "from": "Person:sarah", "to": "Org:acme", "predicate": "works_at" }, { "from": "Person:omar", "to": "Org:acme", "predicate": "works_at" } ] } Switch to direction=out from Person:sarah to walk the other way (Sarah → Acme → …). Use direction=both to ignore edge orientation entirely. How to customize it Param Default Effect start required The entity to begin from, e.g. Org:acme . Missing → SchemaInvalid . direction out out follows subject→object; in follows object→subject; both follows either. depth 2 Maximum hops from start . Why a default cap: graphs can fan out fast — bounding hops keeps a walk cheap and predictable. edge_types all Comma-separated predicate allow-list, e.g. works_at,reports_to . Only these edge types are followed. as_of now Unix-ms timestamp: traverse only edges valid at that instant — and include edges later superseded (live walks see active edges only). For the whole-graph view, GET /api/graph takes ?limit=N (keep the top-N nodes by degree; 0 = full graph) and ?q=term (focus on matches plus their 1-hop neighbourhood, capped at 80 nodes for readability). What it logs The product writes one JSON line per event to its process log. Event Level Keys When abagraph.graph.view info nodes_total , nodes_returned , edges_returned , limit , q Each GET /api/graph call — shows how much of the graph was returned vs. how much exists. The GET /api/walk call does not emit a standalone log line; append ?explain=1 to get an inline query-plan envelope whose stats include reached (nodes visited) and edges_emitted (edges traversed), plus total_ms . How it's isolated This layer owns traversal : given a start node and filters, decide which edges to follow and which nodes are reachable. It enforces four hard rules that bound every walk: Tenant scoped — a walk never traverses an edge belonging to another tenant; it stays inside the caller's namespace. Entities only — only facts whose object is an entity reference are edges; literal-valued facts are invisible to walk. Status aware — retracted edges are always skipped; a live walk follows only active edges, while an as_of walk also follows edges that were valid then but have since been superseded. Time filtered — every candidate edge must be valid at the requested instant. It reads the same facts the fact layer stores; it never writes, never ranks by meaning (that is the Vectors & Semantic Search layer), and never mutates an edge — only follows it. Status Stable. Shipped today: breadth-first GET /api/walk with direction , depth , edge_types , and as_of filters; tenant-scoped traversal; retraction- and validity-aware edge selection; and the GET /api/graph node/edge view with degree-based limit and ?q= focus. Related API Reference: /api/graph & /api/walk Concept: Entities and Predicates — the Type:id convention Concept: Bitemporal Time — what as_of means Layer: Vectors & Semantic Search — recall by meaning instead of relationship Layer: Context Assembly — walk is one input to a context packet ### Indexes & Query Planning (layers) Indexes & Query Planning TL;DR Use this page when a query feels slow, or you want proof of which index ran and how many rows it scanned before filtering. The outcome: a query that pins subject , predicate , or object hits an inverted index and skips the full table — and ?explain=1 shows you that happened. The planner picks one index by a fixed precedence: subject → predicate → object → full scan. First match wins. After the index narrows candidates, remaining conditions ( status , as_of , limit ) are applied as filters; limit stops the scan early. What it does This layer turns a query into a plan: choose the cheapest index that matches your filters, pull those candidates, then apply the rest as filters. The win is concrete — asking for one subject reads only that subject's facts, not the whole store. ?explain=1 makes the choice auditable instead of a guess. Use this page when you are tuning a slow read or verifying that your filter actually engages an index. Quick reference for which index a query uses: I filter on… Planner picks Plan string subject (alone or with others) by-subject index pick by_subject index predicate (no subject) by-predicate index pick by_predicate index object (no subject/predicate) by-object index pick by_object index nothing full scan pick full_scan index Precedence is fixed: if you pass both subject and predicate , the by-subject index runs and predicate becomes a filter. The planner does not estimate cardinality — it follows this order so the plan is predictable. A tiny data example Find Sarah's facts and ask for the plan: curl -H "Authorization: Bearer $TOKEN" \ 'https://nas.abagraph.com/api/facts?subject=Person:sarah&explain=1' { "result": [ { "subject": "Person:sarah", "predicate": "works_at", "object": "Org:acme", "status": "Active", "tx": 42 } ], "explain": { "op": "query_facts", "plan": "pick by_subject index", "stats": { "input.subject": "Person:sarah", "candidates_scanned": 6, "results": 1, "limit_hit": false }, "steps": [ { "phase": "execute", "ms": 0.211 } ], "total_ms": 0.244 } } Add a status filter, as_of , or a small limit and the plan string grows a tail, e.g. pick by_subject index → status filter, as_of time travel, limit early-stop . How to customize it Knob Effect Default which fields you filter on Presence of subject / predicate / object selects the index by the fixed precedence. Why: the most selective key you can name should be subject . full scan if none limit Caps results and lets the scan stop early once the cap is hit. Why: an unbounded list over a large store is slow and heavy; /api/facts caps at 200 if you omit it. 200 on /api/facts status Filter to Active (default), Superseded , Candidate , or Retracted after the index narrows candidates. Active as_of Apply a time-travel filter to the candidate set. Why: reconstruct a past world-state on the same index path. now tx_gt Cursor: return only facts with tx greater than this. Why: poll for just-new changes without rescanning. none ?explain=1 (or true ) Wrap the response in the query-plan envelope. Why: see the index, candidate count, and latency. off The same ?explain=1 flag works on every endpoint. See /docs#api/facts for the full parameter list. What it logs Reads are silent in the process log — there is no per-query event. The instrumentation is the ?explain=1 envelope returned to the caller, not a background log line. Its shape: Field Meaning op The operation, e.g. query_facts , assert_fact , retract_fact . plan The chosen index + filter tail, e.g. pick by_predicate index → limit early-stop . stats candidates_scanned (rows the index returned before filtering), results , limit_hit , plus the echoed inputs. steps Per-phase timings: [{phase, ms, detail?}] . total_ms End-to-end latency for the request. Read candidates_scanned vs results to spot a wide index: a large gap means the index matched many rows that the filters then discarded. How it's isolated Owns: candidate selection (which index) and post-index filtering (status, validity window, as_of , limit early-stop). It is read-only. Hands off to: the Storage layer , which maintains the inverted indexes this planner reads. Semantic (vector) recall is a separate search path, not this index planner. Never touches: writes, conflict resolution, or embeddings. Choosing an index never mutates a fact, and the planner never decides what a write does — it only finds what is already there. Status Stable. Shipped today: the three inverted indexes (subject, predicate, object), fixed-precedence index selection with full-scan fallback, post-index filtering, and the ?explain=1 plan envelope on every REST endpoint. Source-available components are on the roadmap. Storage Layer — where the inverted indexes are built Facts & Bitemporal Time — what status and as_of filter API: /api/facts — query parameters and the explain envelope ### Ingestion & Digest (layers) Ingestion & Digest TL;DR Use this page when you have raw bytes (a document, a company URL, a webhook payload) and want them to become recallable, provenance-bearing facts. Outcome: hand the engine text, it returns a receipt of the triples it wrote — each carries source , confidence , and a validity window — then they are immediately recallable by pattern, walk, or semantic search. Three shipped doors: POST /api/digest (text → facts), POST /api/digest/scrape (company name → facts), POST /api/connectors (register a live source). A candidate judged below the confidence threshold (default 0.75 ) lands as Candidate and does not supersede anything — low-trust extractions never overwrite known-good facts. What it does This is the write-side front door. It takes unstructured input, breaks it into chunks, runs a model extractor to propose subject–predicate–object triples (a triple is one claim: who/what/relation), runs a second model as a judge to score each one, masks any PII, and asserts the survivors as facts. You get back a receipt counting what was written, held as candidate, or superseded — never a silent write. Use this page when you are onboarding a customer, importing a corpus, or wiring a webhook, and you need the bytes to land as facts with provenance attached rather than as an opaque blob. I want to … Call Turn a document or markdown into facts POST /api/digest with content Write a pre-extracted triple batch directly POST /api/digest with facts Onboard a business from just its name or domain POST /api/digest/scrape Register a live data source POST /api/connectors Push one webhook delivery into memory POST /api/connectors/<name>/events Fetch a registered source's URL on demand POST /api/connectors/<name>/pull A tiny data example Send one paragraph about a company: curl -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "content": "Sarah Lee leads RevOps at Acme. Acme sells to mid-market SaaS teams.", "content_type": "text/plain", "source": "crm-export", "extract_model": {"provider":"anthropic","model":"claude-opus-4-5","key":" "}, "judge_model": {"provider":"anthropic","model":"claude-opus-4-5","key":" "} }' \ 'https://acme.abagraph.com/api/digest' The engine writes facts like this one and returns a receipt: Person:sarah --works_at--> Org:acme valid_from = 2026-06-23T00:00:00Z (when it became true; defaults to now) recorded_at = 2026-06-23T00:00:00Z (when the engine learned it) confidence = 0.93 ( = 0.75 threshold - Active) source = "crm-export" { "written": 3, "written_ids": ["01J...","01J...","01J..."], "superseded": [], "candidates": 0, "entities": ["Person:sarah","Org:acme"], "pii_masked": 0 } POST /api/digest/scrape takes only {"company":"acme.com"} , resolves the URL, fetches the public copy via your scraper key, and runs the same pipeline — then adds brand , label , and a recalled preview so the UI can show "we already know…" with no second call. How to customize it Knob Where Effect & why extract_model / judge_model request body Bring your own model keys (BYOK). The key is used only for that outbound call and is never stored or logged. See BYOK Model Keys . threshold request body Confidence cutoff, default 0.75 . A candidate scored below it lands as Candidate and does not supersede; reasserting at full confidence promotes it. Why: keeps low-trust extractions from overwriting good facts. mask request body redact (default), hash_sha256 , or shift_date . PII is masked before storage and before embedding. See PII Masking . content_type / source request body Loader hint ( text/markdown , text/plain , …) and the provenance label stamped on every resulting fact. scraper scrape body BYOK scraper config (e.g. Firecrawl, Tavily). Used only for the one fetch; redacted in logs. facts-batch limit engine Facts-mode accepts at most 500 triples per call; exceeding returns SchemaInvalid before any write. Why: bounds memory and keeps the batch atomic. ABAGRAPH_CONNECTOR_POLL_SECS env var Opt-in scheduled pull interval for registered connectors. Default 0 = off. Why: no background network calls happen unless you ask. Outbound fetch requests (connector pull, scrape) are SSRF-guarded: the managed service blocks all private, loopback, and cloud-metadata addresses. What it logs One JSON line per event. Model and scraper keys appear only in redacted form — the raw key never reaches a log line. Event Level Key fields abagraph.digest info extract_model , judge_model (both redacted), records , content_type , tenant ; facts-mode adds mode , written , superseded , pii_masked abagraph.scrape info scraper (redacted), provider , domain , bytes , extract_model (redacted), tenant abagraph.connector.register info name , kind abagraph.connector.ingest info name , asserted , held , superseded abagraph.connector.poll_start / poll_ingest info interval_secs / per-poll counts (only when the poller is enabled) abagraph.connector.poll_fetch_err / poll_ingest_err warn connector name + error — a scheduled pull failed abagraph.pii.masked info count of tokens masked on this write See Observability & Logs for the shared log model. How it's isolated This layer owns the path from bytes to asserted facts: chunk, extract, judge, mask, assert. It stamps every fact with the tenant derived from the authenticated request — a tenant field in the request body is ignored, so a caller cannot write into another workspace. It hands off conflict resolution (supersede vs. hold as candidate) to the storage layer and vector indexing to the embedding provider. It deliberately never persists your BYOK keys and never retains the raw scraped page beyond the facts and content blob it produced. Read-time tenant filtering belongs to the security layer, not here. Status Stable. Shipped today: on-demand text digest, company-name scrape-to-digest, connector registration with inbound webhook events and on-demand URL pull, and an opt-in scheduled connector pull. Roadmap. Autonomous event-trigger digestion — external events automatically driving re-digestion and enrichment pipelines without a manual call — is not shipped. See Event-Trigger Digestion (roadmap) . PII Masking — what is stripped before a fact is stored or embedded BYOK Model Keys — why your extract/judge keys are never persisted Facts and Triples — the shape of what ingestion writes Observability & Logs — the structured events above ### Observability & Logs (layers) Observability & Logs TL;DR Use this page when you want to know what the engine is doing right now, ship its events to your own sink, or wire a health probe. Outcome: every layer writes one structured JSON line per event to the process log — greppable, sink-agnostic, no parser needed — and three read endpoints summarize the live state. GET /api/observability = recent operational window; GET /api/stats = your data counts; GET /api/health = liveness (unauthenticated). Append ?explain=1 to any endpoint for a query-plan + latency envelope — see what a read did without a profiler. What it does This layer turns engine activity into something you can watch and forward. Each event is one flat JSON object on the process log; the in-process counters roll those events into a recent window you can poll. It is a window since process start , not a durable time-series database — your external log sink remains the system of record. Use this page when you are setting up monitoring, debugging a slow or failing call, or confirming the instance is alive for a load balancer or orchestrator probe. I want to … Call See recent activity (event counts, error rate, freshness) GET /api/observability See my data counts (facts by status, entities, predicates) GET /api/stats Liveness probe (unauthenticated) GET /api/health See a read's query plan and latency append ?explain=1 Ship every event off-box forward the process-log JSON lines to your sink A tiny data example Every event is one line on the process log with the same envelope — ts (ISO-8601 UTC, millisecond precision), level ( info | warn | error ), event (a dotted name), then event-specific fields: {"ts":"2026-06-23T08:42:13.123Z","level":"info","event":"abagraph.digest","records":12,"content_type":"text/markdown","tenant":"acme"} GET /api/observability rolls those into a snapshot (newest-first recent , up to 40 events): { "window": "recent operational window since process start (not a durable TSDB)", "uptime_ms": 3600000, "total_events": 1842, "errors": 3, "error_rate": 0.0016, "events_24h": 1842, "freshness_ms": 210, "by_event": [ {"event":"abagraph.digest","count":61}, {"event":"abagraph.auth.denied","count":2} ], "recent": [ {"event":"abagraph.digest","level":"info","age_ms":210} ] } GET /api/stats is the one observability call that reads your data, scoped to your namespace: { "facts": {"active":214,"superseded":58,"retracted":3,"candidate":11,"total":286}, "entities": 97, "predicates": 22, "tenants": 1, "tenant": "acme", "version": "x.y.z" } How to customize it Knob Where Effect & why process-log forwarding your runtime The product writes one JSON line per event to its process log; capture and forward it to your sink (e.g. Sentry Logs). Why: the window is in-process and resets on restart — durable retention is yours to own. ?explain=1 any endpoint Wraps the response as {result, explain:{op, plan, stats, steps, total_ms}} . Why: see the chosen plan and latency inline, no profiler. GET /api/health endpoint Unauthenticated and payload-free, returns {"ok":true} — safe to point a load balancer or orchestrator probe at. GET /api/observability recent size endpoint Returns the most recent 40 events newest-first; by_event counts are unbounded for the window. No separate audit log is needed for data history: time-travel reads plus the provenance on each fact replace it. What it logs This layer both emits the shim and surfaces a few lifecycle events of its own. Every other layer's events flow through the same shim and are counted in GET /api/observability . Event Level Meaning & key fields abagraph.server.start info The instance booted. abagraph.server.accept_error warn A connection accept failed. abagraph.sse.subscribe info A client subscribed to the live fact feed. abagraph.ttl.swept info Transient facts past their valid_until were hard-deleted. Levels are the alerting contract: error lines drive the errors / error_rate fields in the snapshot, so a sink rule on level=error is your first-line alert. How it's isolated This layer owns the operational-telemetry surface — counts, the recent window, health, and the query-plan envelope — and reads only metadata: event names, levels, timestamps, and per-namespace counts. It never reads fact payloads, embeddings, or secrets, which is why an instance snapshot is safe to serve any authenticated caller. GET /api/stats is the only observability endpoint scoped to your data (it counts your namespace; an admin sees the whole graph); GET /api/observability and GET /api/health are instance-level and payload-free. It hands durable retention off to your external sink and deliberately is not a metrics store. Status Stable. Shipped today: structured one-line-per-event logging across every layer, the GET /api/observability snapshot (uptime, totals, error rate, 24h count, freshness, per-event counts, recent ring), GET /api/stats tenant-scoped data counts, the unauthenticated GET /api/health probe, and ?explain=1 on every endpoint. A durable in-product time-series store is out of scope by design — your sink is the record. Ingestion & Digest — the write events counted here Security & Governance — the auth events counted here ### Security & Governance (layers) Security & Governance TL;DR Use this page when you need to know who can read what, how workspaces are kept apart, and what is stripped before data lands. Outcome: every request is reduced to one claim — admin , or a single namespace (tenant) — and every read is filtered to that claim before a fact leaves the engine. A tenant key is bound to its subdomain: used on another tenant's subdomain it is rejected with 404 (not 401 ) — a custom URL can't be turned into a cross-tenant read. PII (emails, phones, SSNs, card numbers) is masked before storage and before embedding. Scope is honest and heuristic — see the limits below. What it does This is the boundary every request crosses before it touches data. It answers four questions in order: who are you? (bearer token or session cookie), which namespace may you see? (the tenant filter), what must be stripped? (PII masking), and can two agents collide on a resource? (concurrency leases). A namespace (used interchangeably with tenant ) is a named workspace stamped on every fact at write time. Use this page when you are provisioning keys, designing multi-tenant separation, or auditing what governance the platform enforces for you versus what you must do upstream. I want to … Mechanism Authenticate a request Authorization: Bearer <key> header, or the session cookie Scope all data to one workspace the key's namespace, bound to its subdomain Strip PII before it is stored or embedded mask field on any ingest call Stop two agents clobbering a resource POST /api/leases Use a model key without it being stored BYOK model config (per call) A tiny data example One key, two subdomains — the same credential, two outcomes: # Key issued for namespace "acme": curl -H "Authorization: Bearer $ACME_KEY" 'https://acme.abagraph.com/api/facts' # - 200, returns only facts where tenant == "acme" curl -H "Authorization: Bearer $ACME_KEY" 'https://other.abagraph.com/api/facts' # - 404 (tenant key rejected on another tenant's subdomain) Every fact the acme caller wrote is stamped with the namespace, and that stamp is what the read filter matches: Person:sarah --works_at--> Org:acme tenant = "acme" # A read on acme.abagraph.com sees it; a read as namespace "other" never does. A concurrency lease is itself a fact — one active lease per resource, so a re-acquire supersedes the prior holder: curl -X POST -H "Authorization: Bearer $ACME_KEY" \ -d '{"resource":"nightly-report","holder":"agent-7","ttl_secs":300}' \ 'https://acme.abagraph.com/api/leases' # held for 300s; past expiry it reads back as "stale" How to customize it Knob Where Effect & why Admin key / per-tenant key managed service Admin key — spans all namespaces; per-tenant key — scoped to one namespace; both issued by the managed service. mask ingest body redact (default), hash_sha256 , shift_date . Chosen per ingest call. See PII Masking . ttl_secs lease body Lease lifetime, default 300 . Past expiry the lease reads back as stale so a crashed holder never blocks forever. ABAGRAPH_MAX_BODY_BYTES / ABAGRAPH_MAX_CONCURRENT_UPLOADS env var Bound request size and concurrent uploads. Why: a denial-of-service guard on ingest throughput. Deeper references: Authentication & Authorization , Tenant Isolation , and BYOK Model Keys . What it logs One JSON line per event. No key, secret, or fact payload ever appears — BYOK keys are redacted and the secret vault stores references only, never values. Event Level Meaning & key fields abagraph.auth.denied warn A request was rejected; path . Returned as 404 so the gated surface isn't advertised. abagraph.auth.login_gate info An anonymous page request to a private workspace got the login wall; path . abagraph.pii.masked info Count of tokens masked on a write. abagraph.tenant.create / abagraph.tenant.delete info A namespace was provisioned or removed. abagraph.admin.purge info An admin-scoped purge ran. How it's isolated This layer owns the request boundary and nothing past it: identity (auth), visibility (the namespace filter applied to every read), redaction (PII masking before storage/embedding), and resource coordination (leases). It hands an authenticated, namespace-stamped request to the ingest and query layers and lets them do the data work. The same namespace filter is enforced on the MCP tool surface, always — even for an admin caller. Branch-overlay facts are excluded from the main read path regardless of namespace. It deliberately never stores raw secret or model-key values, and it never interprets the meaning of your data — it only stamps and filters by namespace. Status Stable. Shipped today: bearer auth bound to a subdomain plus cookie sessions; namespace isolation on every read and write and on the MCP surface; partial (heuristic, token-level) PII masking covering emails, phones, SSNs, and card numbers; concurrency leases with TTL and an immutable audit trail; a secret-reference vault and policy simulation. Honest scope of PII masking. Detection is heuristic on whitespace-delimited tokens — PII glued inside a longer word may pass, and numeric, boolean, and blob values are not scanned. Pre-scrub upstream if you need stricter coverage. Roadmap. Customer-defined policy-as-code — declarative rules that gate reads and writes — is not shipped. Authentication & Authorization — bearer tokens, cookie sessions, public endpoints Tenant Isolation — the read/write chokepoint in detail PII Masking — classifiers, strategies, and limits Ingestion & Digest — where the namespace stamp is applied ### Storage Layer (layers) Storage Layer TL;DR Use this page when you are deciding where your facts live (volatile vs durable), or you need to know what survives a restart and what does not. The outcome: every committed write is durable before the call returns — facts survive process restarts with no separate backup step. Storage is the durable, append-only record. It does one thing: persist and return facts. It never interprets predicates, validity windows, or conflicts. Selection is a deployment knob: no data path = in-memory (volatile); a .json / .jsonl / .log path = human-readable append-log; a directory path = durable, crash-safe file-backed store (production default). One write transaction = one atomic batch. A supersession is two puts (close the old fact, open the new) committed together — never half-applied. What it does The storage layer is the system of record: it persists facts so they survive a restart, and on open it replays them to rebuild the read indexes the query layer needs. Reads are served from those in-memory indexes, so durable storage costs you write latency, not read latency. Use this page when you want to know whether your data is volatile or durable, what a crash leaves behind, or why a write is atomic. Everything above this layer (validity time, conflict rules, query planning) trusts storage to do exactly one job: put a fact and give it back later, unchanged. Writes are append-only — a fact is immutable once written. The only "change" to a fact is a new write that records a status transition (for example Active to Superseded). History is never overwritten in place, which is what makes time-travel reads possible. A tiny data example Assert one triple — Person:sarah --works_at--> Org:acme : curl -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme","confidence":0.95,"source":"hr-system"}' \ 'https://nas.abagraph.com/api/facts' On a durable deployment the engine hands storage a one-entry batch and flushes it before returning. If this write had contradicted an existing active fact, the batch would instead carry two entries — the closed prior fact and the new one — committed atomically: {"op":"put","fact":{"subject":"Person:sarah","predicate":"works_at","object":"Org:globex","valid_until":1718600000.0,"status":"Superseded","tx":58}} {"op":"put","fact":{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme","valid_until":null,"status":"Active","tx":58}} Both rows share one tx number, so a consumer polling with ?tx_gt=57 sees the whole change or none of it. How to customize it Knob Effect Default data path (deployment flag) Omit = in-memory, lost on restart. .json / .jsonl / .log = human-readable log format, portable. Directory path = durable, crash-safe store with blob storage (production). in-memory ABAGRAPH_TTL_INTERVAL_SECS How often the sweeper hard-deletes expired transient facts. Why: tune down for tighter TTL cadence, up to reduce sweep frequency. 30 ABAGRAPH_MAX_BODY_BYTES Largest write payload accepted before a request is rejected. Why: caps memory per connection. 64 MiB per-fact transient: true + valid_until Marks a fact for TTL hard-delete once valid_until passes. Why: Redis-style ephemeral memory without keeping Superseded history. false (history kept forever) The managed service handles durability and consistency for you. What it logs The product writes one JSON line per event to its process log (each line carries ts , level , event ). A normal write or batch append is intentionally silent — the durable record is the data file itself, not the log stream, so the log stays signal-only. The durability events you will see: Event Level Fields Meaning abagraph.ttl.swept info removed Transient facts hard-deleted in one sweep tick (only emitted when > 0). How it's isolated Owns: the durable bytes — put, get, scan, batch, and the inverted lookups (by subject, predicate, object) it rebuilds on open. It guarantees a batch commits all-or-nothing and that a written fact reads back byte-identical. Hands off to: the Facts layer above, which decides what a fact means (its two time axes and status), and the Conflict layer , which decides what a new write does to prior facts. Storage just receives the resulting batch. Never touches: predicate semantics, validity-window math, conflict resolution, or tenant scoping. A closed (Superseded) fact reaches storage as an ordinary put — storage does not know or care why. Status Stable. Shipped today: volatile in-memory storage, the human-readable append-log, and the durable crash-safe file-backed store with blob storage. Reads are served from rebuilt in-memory indexes; durable writes flush before returning. Facts & Bitemporal Time — what a stored fact means Indexes & Query Planning — how reads use the rebuilt indexes ### Vectors & Semantic Search (layers) Vectors & Semantic Search TL;DR Use this page when you want to recall a fact by meaning ("who handles billing?") rather than by exact subject/predicate — and to understand what powers POST /api/search . An embedding is a list of numbers that captures the meaning of text; two texts that mean similar things have embeddings that point in similar directions. kNN (k-nearest-neighbours) returns the k facts whose embeddings are closest to your question's embedding — default k = 8 . Embeddings are off by default : bring your own provider via ABAGRAPH_EMBEDDING ( hash | openai | http ). With none configured, search returns an empty result. Search is ranked by cosine similarity and filtered inside the scan, so tenant isolation and time-travel ( as_of ) compose correctly with the top- k cut. What it does Use this layer when an agent knows roughly what it is looking for but not the exact triple — e.g. "find anything about pricing objections" should surface Person:sarah --raised_concern--> "price too high" even though the words "pricing objection" never appear in the fact. When an embedding provider is configured, asserting a fact auto-embeds its text: the engine stores a vector alongside the triple. At query time, POST /api/search embeds your natural-language question with the same provider and ranks every candidate fact by cosine similarity — a score in [-1, 1] where 1.0 means "pointing the same way" (near-identical meaning) and 0.0 means unrelated. The top k facts come back, highest score first. Two terms, defined once: Embedding — a fixed-length array of floats produced by an embedding model; a numeric fingerprint of meaning. Dimension is set by the provider (e.g. 64 for the built-in hash provider, 1536 for OpenAI's text-embedding-3-small ). kNN / k-nearest-neighbours — the search that returns the k stored vectors closest to a query vector. "Closest" here means highest cosine similarity. A tiny data example Assert a fact (auto-embedded when a provider is configured): curl -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"raised_concern","object":"price is too high for our budget","confidence":0.9,"source":"call-2026-06-20"}' \ 'https://api.abagraph.com/api/facts' Later, recall it by meaning — note the question shares no words with the stored object: curl -H "Authorization: Bearer $TOKEN" \ 'https://api.abagraph.com/api/search?question=pricing+objections&top_k=5' { "kind": "search_result", "question": "pricing objections", "answer_facts": [ { "subject": "Person:sarah", "predicate": "raised_concern", "object": "price is too high for our budget", "confidence": 0.9, "source": "call-2026-06-20", "status": "Active" } ], "entities": [] } The raw embedding vector is never returned — responses ship the human-readable triple plus provenance, not the float array. How to customize it Knob Where Default Effect ABAGRAPH_EMBEDDING env var unset (off) Selects the provider: hash (built-in, deterministic, zero-cost — good for testing), openai (OpenAI's embeddings API), or http (any OpenAI-compatible HTTP endpoint = BYOK). ABAGRAPH_EMBEDDING_API_KEY env var falls back to OPENAI_API_KEY Your provider key. If empty for openai / http , embeddings stay disabled. ABAGRAPH_EMBEDDING_BASE_URL env var https://api.openai.com/v1 Point at any OpenAI-compatible host (self-managed model gateway, alternate vendor). ABAGRAPH_EMBEDDING_MODEL env var text-embedding-3-small Model name passed to the provider. ABAGRAPH_EMBEDDING_DIM env var 64 ( hash ) / 1536 ( openai / http ) Vector dimension. Must match what the model emits, or facts and queries won't compare. top_k per request 8 How many nearest facts to return. GET /api/search?top_k=20 or the JSON body field. as_of per request now Unix-ms timestamp: search the world as it was known then (see time-travel ). Why dimension must match: cosine similarity compares vectors element by element — a query embedding and a fact embedding of different lengths cannot be scored, so that fact is silently skipped. Pick one provider/model per dataset. See Configuration: Embedding Providers for the full provider matrix. What it logs The product writes one JSON line per event to its process log (forward it to your log sink). Event Level Keys When abagraph.embedding.disabled warn reason (e.g. "no_api_key" ) At startup, when openai / http is selected but no key is present — embeddings stay off and search returns empty. abagraph.server.start info bind , auth_enabled , max_body_bytes , max_concurrent_uploads Once at boot — confirms the process is up. The search call itself does not emit a standalone log line. To inspect a single query's plan and latency, append ?explain=1 and read the inline explain envelope (op, plan, stats, total_ms ). How it's isolated This layer owns vector math and ranking : it embeds text, stores per-fact vectors, and scores candidates by cosine similarity. The tenant and bitemporal filters run inside the kNN scan — not as a post-filter on a global top- k — so a tenant only ever ranks its own facts and an as_of query only ranks facts valid at that time. This is the same isolation boundary every read route enforces. It hands the actual storage and indexing of triples to the fact layer, and it deliberately never ships embeddings to clients, never mutates facts, and never crosses tenant boundaries. Graph traversal (the Graph & Walk layer) is a separate concern — search ranks by meaning, walk follows typed edges. Status Stable. Shipped today: per-fact auto-embedding on assert, cosine kNN over POST /api/search with top_k and as_of , tenant-scoped results, and three BYOK provider options ( hash , openai , http ). Embeddings are opt-in — with no provider configured, facts store no vector and search returns an empty result set rather than erroring. Related Concept: Embeddings and Search API Reference: /api/search Layer: Graph & Walk — recall by relationship instead of meaning Configuration: Embedding Providers ### Glossary (reference) Glossary TL;DR Use this page when a term on another page is unfamiliar — every concept the docs use is defined here in one plain clause. Entries are alphabetical and assume no prior knowledge. Most entries carry a tiny concrete example; bookmark this page, every other page assumes it. Active A fact's status when it is currently valid and counts during conflict resolution; the default state of a fresh, confident assertion. Contrast Candidate . AIhouse The category abagraph defines — the system-of-record for agent memory , the way the data warehouse and lakehouse were systems-of-record for analytics. as_of / time-travel A query that rewinds the whole store to a past instant and returns the facts that were Active then — including ones since superseded — so you can ask "what did we believe last Tuesday?" with no separate audit log (e.g. ?as_of=2026-01-05T00:00:00Z ). Bitemporal Tracking two independent time axes per fact — valid time (when it was true in the world) and record time (when the system learned it) — so "when was it true?" and "when did we know it?" are answerable separately. Branch An instant fork of a tenant's memory as an overlay you can write to in isolation, then diff, promote back, or discard — like a git branch for an agent's beliefs. build_context The call that returns a context packet for a task — available as POST /api/context and as the build_context MCP tool. Candidate A fact's status when it was asserted below the confidence threshold (default 0.75): it is stored and searchable but does not supersede an existing Active fact; re-asserting at full confidence promotes it to Active. WHY: low-trust hints accumulate without overwriting what you are sure of. Confidence A score from 0.0 to 1.0 on every fact saying how sure you are; at or above the threshold (default 0.75) the fact is Active , below it the fact is a Candidate . Connector A configured integration that pulls data from an external source — a database, a document store, or a scraped page — and lands it in abagraph as facts (see digest ). Context packet A single token-budgeted, confidence-ranked bundle of the facts (plus graph walk and wiki pages) most relevant to a goal — load it at the start of an agent turn instead of stuffing the prompt by hand. digest Turning raw text or a batch of pre-extracted claims into stored facts in one call — the fast path from a document or a connector to memory. Edge A fact whose object is another entity , forming a relationship the graph can walk (e.g. Person:sarah --works_at--> Org:acme ); the predicate is the edge type. Embedding A numeric vector that captures a fact's meaning, generated automatically on write, so the engine can find facts by similarity even when the exact words differ. Entity A thing facts are about, named by convention Type:id (e.g. Person:sarah , Org:acme ); the colon is a convention, not a requirement. Fact The core unit of knowledge: a triple (subject–predicate–object) plus provenance — valid time, record time, confidence, and source — that is never overwritten, only superseded (e.g. Person:sarah --works_at--> Org:acme , confidence=0.95 , source="hr" ). fact_view The bounded, provenance-rich shape an MCP tool returns for a fact — id, subject, predicate, object, confidence, source, status, validity — and never the raw embedding vector. Graph walk Following edges from a starting entity outward (or inward) to a chosen depth to answer relationship questions like "who reports to Sarah's manager?" without separate keyword searches. kNN / semantic search "k nearest neighbours" — given a natural-language query, the engine embeds it and returns the k facts whose embeddings are closest in meaning. Lease A time-bounded exclusive hold an agent takes on a resource so two agents do not act on the same thing at once; it expires automatically if it is not renewed. Namespace / tenant The isolation unit for facts: reads filter on it, writes stamp it, and conflict resolution stays inside it, so two agents on different namespaces never collide; it is called tenant in the REST API and namespace in the MCP API. Object The value side of a triple — either a literal (text, number, boolean) or a reference to another entity (e.g. Org:acme in Person:sarah --works_at--> Org:acme ). PII masking Automatically redacting personally identifiable information (emails, phone numbers, and similar) from fact values so sensitive data is not stored or returned in the clear. Predicate The relationship name in a triple (e.g. works_at , tag ); a functional predicate allows one active object per subject, a set-valued one allows many. Provenance / source Where a fact came from — the optional source string (a URL, agent name, document id, …) that lets an agent justify or audit any claim (e.g. source="onboarding-form" ). Record time (recorded_at) When the system learned a fact, set automatically to the wall clock at write time and not overridable — the second axis of bitemporality . Status A fact's lifecycle state: Active (valid now), Candidate (below threshold), Superseded (closed by a newer conflicting fact, visible only via as_of ), or Retracted (explicitly removed, hidden by default). Subject The entity a triple is about — the left-hand side (e.g. Person:sarah in Person:sarah --works_at--> Org:acme ). Supersession The default conflict resolution: asserting a contradicting value closes the old fact (sets its valid_until and marks it Superseded ) and opens the new one in one atomic write, so history is preserved rather than overwritten; it is always tenant -local. transact An atomic multi-fact write with compare-and-swap semantics ( POST /api/transact ): either every change in the batch applies or none does. Triple The subject–predicate–object shape every fact takes — " who has what relation to what " (e.g. Person:sarah | works_at | Org:acme ). tx A monotonic write-transaction number stamped on every fact a single write touches; consumers replay only new changes by asking for facts with tx greater than the last one they saw ( ?tx_gt=… ). Valid time When a fact was true in the world, expressed as the interval valid_from … valid_until — the first axis of bitemporality . valid_from The start of a fact's valid-time window — when the claim became true in the world (defaults to the write time if you do not supply it). valid_until The end of a fact's valid-time window; absent / open-ended means the fact is still true now, and supersession sets it when a newer value arrives. Related pages Mental Model — these terms in context What is abagraph? — the big picture Guide: Assert a Fact — the data model in practice Facts and Triples — field-by-field detail ### Release Notes (reference) Release Notes TL;DR abagraph v0.1.0 ships four independently usable API surfaces over one storage layer. This page describes what is available now in the managed service. v0.1.0 — Current release abagraph is a proprietary, managed bitemporal graph and vector memory engine for LLM agents, delivered as a hosted service. Four surfaces are available: Surface 1 — Fact API High-level subject–predicate–object assertions with full bitemporal tracking, accessible over the REST API at /api/facts . Assert — POST /api/facts . Conflict policy applied per-request (default AutoSupersede ). Facts below the confidence threshold (default 0.75) land as Candidate without superseding. Retract — DELETE /api/facts/:id . Sets status to Retracted ; history is preserved. Query — GET /api/facts . Filter by subject, predicate, object, status, tenant, limit, and tx_gt cursor. Semantic search — POST /api/search . kNN over stored embeddings; requires an embedding provider to be configured. Graph walk — GET /api/walk . BFS with direction (outbound/inbound/both), edge-type filter, and depth limit; supports as_of time-travel. Time travel — ?as_of=<timestamp_ms> on query endpoints. Read the world as it was at any past millisecond; returns superseded facts that were active at that time. Context packet — POST /api/context . One-call assembly of task-relevant facts, adjacent graph nodes, wiki pages, corroboration data, and token-budget compaction. Atomic compare-and-swap — POST /api/transact . Compare guards, asserts, and retracts in one batch; mismatch returns CompareFailed with the failing guards. Embedding providers (selected by ABAGRAPH_EMBEDDING on dedicated deployments): hash — deterministic, no API key, suitable for development and testing. openai — OpenAI-compatible REST endpoint. Requires ABAGRAPH_EMBEDDING_API_KEY (falls back to OPENAI_API_KEY ). Default model text-embedding-3-small , default dimension 1536. Surface 2 — Attribute Schema Layer A schema-driven attribute store that underpins how facts are recorded and validated. Each entry carries an entity, attribute, value, transaction ID, and operation, with explicit attribute definitions covering value type, cardinality, and uniqueness. Attribute schema — registers an attribute with an identifier, value type ( Ref | Long | Double | Str | Bool | Inst ), cardinality ( One | Many ), and a unique flag. Cardinality-one attributes are automatically superseded on re-assertion; cardinality-many attributes accumulate. Time travel — point-in-time reads using as_of work at this layer too, returning the attribute values that were active at that moment. Graph integrity checks — the engine surfaces orphans, dangling references, and contradictions via GET /api/factcheck . Open-ended entries — entries without a defined end time remain active until explicitly retracted or superseded. Surface 3 — Agent Kernel Durable, resumable agentic loop layered over the Fact and Attribute stores. Runs are crash-safe: each step is committed atomically, so a run can be resumed from its last completed step after any interruption. Spec / Run / Step — strongly-typed entities; each step is one atomic commit, making runs crash-safe and resumable. Per-turn context — assembles the spec goal, recent steps verbatim, older steps compressed by a configurable summariser, and semantically recalled facts from prior turns. Model integration — the kernel drives a configurable model integration that returns a message, tool call, or done signal. Connect any LLM provider by implementing the model port. Tool integration — tools return a result or a require-approval signal. A require-approval outcome parks the run as AwaitingApproval ; callers resume by re-entering the run. Budget governor — configurable step count and token limits; an exhausted budget closes the run as Budget . Verifier — optional post-completion quality gate; passes or fails the run based on output criteria. Run outcomes — Success , Failure , Budget , AwaitingApproval . Memory mirroring — step content is indexed into the fact store on each commit so later turns can recall prior steps via semantic search. Surface 4 — MCP "orchestrate" Server Exposes the engine to any MCP-compatible LLM client via JSON-RPC 2.0 (protocol 2025-06-18 ). MCP is available over HTTP at POST /mcp on the managed endpoint, auth-gated identically to the REST API. Seven tools, all namespace-scoped. Tool names and their required fields: Tool Purpose Required params digest Bulk-ingest pre-extracted facts into a namespace facts[], namespace remember Assert a single fact subject, predicate, object, namespace recall Retrieve facts — semantic (kNN), pattern, or walk mode, namespace recall_as_of Point-in-time snapshot query as_of, namespace whats_changed Diff between two timestamps from, to, namespace build_context One-call task context packet (facts + walk + pages + compaction) task, namespace explain_fact Full history and provenance of one fact fact_id, namespace Tool profiles via ABAGRAPH_MCP_PROFILE : agent-memory (build_context, digest, remember, recall, whats_changed), read-only (recall, recall_as_of, explain_fact), unset = all seven. Responses use fact_view — provenance-rich but never raw embeddings. REST server The managed REST endpoint speaks HTTP/1.1 with JSON request and response bodies. On dedicated deployments, the listen address and persistence path are controlled via the ABAGRAPH_BIND and ABAGRAPH_DATA environment variables. Notable endpoint groups available at /api/ : Facts — GET|POST /api/facts , DELETE /api/facts/:id , POST /api/transact . Context and search — GET|POST /api/context , GET|POST /api/search , GET /api/graph , GET /api/walk . Digest pipeline — POST /api/digest (raw content or pre-extracted facts; PII masking, BYOK judge, confidence-based candidate hold). Utility — POST /api/compact , POST /api/enrich , POST /api/consolidate , GET /api/factcheck . Branches — full CRUD on /api/branches ; diff, promote, and discard operations. Tenants (admin) — /api/tenants lifecycle management. Connectors — /api/connectors ; SSRF-guarded fetch, field-to-fact mapping, webhook events, pull. Stream — GET /api/stream ; Server-Sent Events fact feed ( data:{op,tx,fact} , keepalive ~15 s). Connect — GET /api/connect ; returns tenant metadata and ready-to-use code snippets (curl, TS, CLI, agent, MCP). Machine-readable surfaces — /llms.txt , /llms-full.txt , /openapi.json , /index.md , /.well-known/api-catalog (RFC 9728), /sitemap.xml . ?explain=1 on any endpoint wraps the response in a query-plan + latency envelope. TypeScript SDK Package @abagraph/client v0.1.0. Node >= 18, zero runtime dependencies. Three entry points: "." (REST client), "./agent" ( createAgentMemory ), "./plugin" ( memoryPlugin ). createClient({ baseUrl, apiKey }) — full REST surface: health, assert, retract, query, transact, context, search, graph, walk, factcheck, connect, digest, compact, enrich, consolidate, branches, tenants. Error type: AbagraphError { status, code } . createAgentMemory({ baseUrl, apiKey, namespace, agent, ... }) — session-start loadContext() , remember() , digest() , compact() , enrich() , consolidate() , and a turn(task, fn) helper (load → run → remember). One structured log line per operation via onLog . memoryPlugin(opts) — ADK-style runner hook: onSessionStart / onSessionEnd . Node CLI Package @abagraph/cli , binary abagraph , runnable via npx abagraph . Commands: init , assert , query , branch (create/list/diff/promote/discard), connect , factcheck , agent init , consolidate , help . Config precedence: flag > ABAGRAPH_URL / ABAGRAPH_KEY env vars > ~/.abagraph.json . Wiki layer Parses [[Entity:foo]] wikilinks in prose into references edge facts; attaches cites facts with quoted passages in properties . Submit markdown content containing wikilink syntax to POST /api/digest to trigger automatic link extraction. Glossary — definitions for every term on this page What is abagraph? — the big picture Guide: Assert a Fact — hands-on with Surface 1 Mental Model — bitemporality explained ### Customer-Defined Governance & Policy-as-Code (roadmap) Customer-Defined Governance & Policy-as-Code TL;DR Use this page when you need to control which agents and people may read or change which memory, and you want that policy to be your own declarative data — not something hard-coded in the service. Roadmap — not yet generally available. Source-available components are on the roadmap. Outcome: ship a security policy as data, version it, and have the engine enforce it on every access — no redeploy to change who can do what. Policy-as-code = your access rules expressed as a versioned declarative artifact the engine evaluates, rather than logic baked into the product. Core stance: check permissions, not roles ; deny-by-default ; isolation at the data layer ; maker-checker on destructive admin actions. The five rules, with thresholds Rule What it means Why Permission-based access Checks ask "has facts:write ?", never "is an admin?". Roles map to permissions in one place. One mapping table is the single source of truth; adding a permission never means hunting role checks across the codebase. Deny-by-default / fail-closed No matching allow rule → denied. An error in a check → denied. A bug or outage must lock down, never fling open. Tenant isolation at the data layer Isolation is enforced once at the data-access boundary, not re-implemented per query. One chokepoint can't be forgotten in a new endpoint; a missed per-query filter would leak across tenants. Event-driven cache invalidation Policy decisions cache for a 1–5 min TTL , invalidated on policy-change events; sensitive ops bypass the cache . A revoked permission must take effect fast; sensitive reads can't trust a stale yes. Maker-checker on destructive admin Destructive admin actions need a second principal to approve, plus re-auth . No all-powerful admin. One compromised or mistaken account can't irreversibly destroy data alone. Permissions, not roles A permission is a single capability ( facts:read , facts:write , branch:promote ). A role is a named bundle of permissions. Rule: authorization code checks the permission; the role→permission mapping lives in exactly one place. Why: when you add a capability you edit one table, and every call site that already checks the right permission inherits it — no scattered "if role == admin" to find. Expressed as policy data, a customer-owned rule reads like a fact about who may do what: Role:analyst --grants-- Permission:facts:read source="policy-v7" Role:analyst --grants-- Permission:facts:write source="policy-v7" Permission:branch:promote --requires-- Approval:maker-checker source="policy-v7" Deny-by-default and fail-closed Rule: a request is allowed only if a policy rule explicitly permits it; the absence of a rule, or any error while evaluating one, results in a denial. Why: security that fails open turns a transient bug into a breach. A check that throws is treated identically to a check that said "no". Isolation at the data layer Every read is scoped to the caller's tenant (namespace) at a single data-access chokepoint, so an endpoint author cannot forget to add a per-query filter. A new route inherits isolation for free. Why: per-query filtering is the classic source of cross-tenant leaks — one missing WHERE and a customer sees another's memory. Maker-checker for destructive actions Maker-checker = one principal proposes an action, a different principal approves it before it executes. Rule: destructive admin actions (bulk purge, tenant deletion, policy rollback) require a second approver and a fresh re-authentication. Why: there is deliberately no single all-powerful admin who can erase data unchecked. Related Tenancy and Isolation — the data-layer scoping you have today Roadmap: Durable Workflows & DAGs — approval gates inside a run Roadmap: Source-Available Components — what may open later ### Durable Workflows & DAGs (roadmap) Durable Workflows & DAGs TL;DR Use this page when you are planning multi-step agent work that must survive a crash, a restart, or a fan-out/fan-in DAG, and you want to know what abagraph will run it on and how it stays correct. Roadmap — not yet generally available. Source-available components are on the roadmap. Outcome: kick off an agent task once and trust it to run to completion — resuming from the exact step it died on, never repeating a side effect — directly over your agent memory. Honest status: the durable run records (a task spec, a run, and per-step records) already persist as provenance-bearing facts; the public runs API is not yet served . Planned engine: Temporal — a durable-execution framework that re-runs your code from saved progress after a failure. What you will be able to do Start a long-running agent task — research, enrich, multi-tool plans — and get an outcome guarantee, not a best-effort fire-and-forget. The task is a workflow : a durable function whose progress is checkpointed, so a process restart resumes mid-flight instead of starting over. Each side-effecting unit is an activity : one step the workflow calls (assert facts, call a tool, hit an external API). Wire activities by dependency and you have a DAG (directed acyclic graph) with fan-out and fan-in, e.g. enrich ten entities in parallel, then merge. Quick reference I want to … Concept Status Run a task that survives a crash Workflow (durable function) roadmap Do one side-effecting step Activity roadmap Run steps in parallel then merge DAG (fan-out / fan-in) roadmap See what a run did, step by step Run + per-step records (persist today) partial Pause for a human decision Approval gate (maker-checker) roadmap What persists today The data model is already live: a task spec (the recipe), a run (one execution of it), and a step record per turn are written as durable facts you can recall and time-travel. A tiny example of what a run looks like in memory: Run:r-42 --status-- "Running" valid_from=… recorded_at=… confidence=1.0 source="orchestrator" Run:r-42 --has_step-- Step:r-42.3 valid_from=… recorded_at=… confidence=1.0 source="orchestrator" Step:r-42.3 --produced-- Person:sarah valid_from=… recorded_at=… confidence=0.9 source="enrich-tool" Because steps persist as facts, a run is auditable and resumable: re-entering a run replays from the last recorded step index. What is not yet exposed is a public API to define a spec, launch a run, and query its state over REST/MCP — that is the work this page describes. How it will work — the reliability rules These are not aspirations; they are the rules the workflow layer is being built to. Each states the threshold and the one-line reason. Idempotency lives in the activity, not the broker A broker (the queue that hands work to consumers) delivers at-least-once by default — the same message can arrive twice. So idempotency (an operation safe to run more than once with the same result) must be enforced where the effect happens. Rule: an idempotency key is a unique row written in the same transaction as the effect , keyed on a business id — the run id. Why: the unique constraint makes the second delivery a no-op without consulting a second store. # written atomically WITH the side effect, in one batch: Run:r-42 --idempotency_key-- "send-welcome-email:r-42" confidence=1.0 source="orchestrator" Bounded retries at exactly one layer Rule: retries are bounded to ≤ 3 , with exponential backoff + jitter (random delay added to each backoff), applied at exactly one layer . Why: retrying at two layers multiplies attempts (3 × 3 = 9) and amplifies load; jitter spreads retries so a recovering dependency is not hit by a synchronized thundering herd. Never dual-write the DB and the broker Rule: never write the database and publish to the broker as two separate writes. Use a transactional outbox (write the "to-send" message into the same DB transaction as the state change, then a relay forwards it) or change-data-capture (stream the database's commit log as events). Why: a crash between two independent writes leaves them permanently disagreeing. A dead-letter queue on every side-effecting consumer Rule: every consumer that causes a side effect has a dead-letter queue (a holding queue for messages that exhausted their retries) with an alert on depth > 0 . Why: depth > 0 means at least one effect is stuck — silence would hide data loss. The governing principle: violations of integrity are perpetual inconsistency — build for integrity first. Related Roadmap: Event- & Trigger-Based Digestion — what kicks a workflow off Roadmap: Customer-Defined Governance — approval gates for destructive steps Bitemporal Time — why every step record is replayable ### Event- & Trigger-Based Digestion (roadmap) Event- & Trigger-Based Digestion TL;DR Use this page when you want agent memory to refresh itself the moment source data changes, instead of remembering to call digest by hand. Roadmap — not yet generally available. Source-available components are on the roadmap. Outcome: a new CRM contact, a nightly export, or an inbound webhook turns into provenance-bearing facts automatically — your agents read fresh memory without an ingestion cron you maintain. Digest = the pipeline that turns raw content (text, a document, a record) into typed facts with source and confidence attached. Today, manually: POST /api/digest (one-shot) and POST /api/connectors/:name/events (inbound webhook delivery). Automatic event/trigger-driven digestion is the gap this page describes. Three trigger kinds I want memory to update … Trigger Plain meaning the instant a source row changes on new data (change-data-capture) Change-data-capture = streaming the database's commit log as events, so every committed insert/update/delete fires ingestion. on a fixed cadence on a schedule A cron-like timer pulls and digests a source at an interval (e.g. every 15 min). when an external system pushes on a webhook An external service sends an HTTP callback; the payload is digested on arrival. A tiny concrete example A CRM fires a webhook when a contact's employer changes. The trigger digests the payload and the engine asserts: Person:sarah --works_at-- Org:acme valid_from=1718000000000 recorded_at=1718000001200 confidence=0.9 source="crm-webhook" Because the assert carries valid_from (when it became true) and recorded_at (when memory learned it), a later contradicting update — Sarah moves to Org:globex — automatically closes the old fact and opens the new one. You can still ask "where did we think Sarah worked last Tuesday?" with no separate audit log. Confidence rule: a digested fact at confidence >= 0.75 lands Active ; below 0.75 it lands Candidate and does not supersede, so a noisy source cannot overwrite a trusted one. What you can wire today The building blocks are live; only the automatic triggering is roadmap. Manual: POST /api/digest — load → chunk → extract → judge → assert, in one call. Webhook (manual delivery): register a connector, then push events to POST /api/connectors/:name/events . The body may be one JSON object, a JSON array, or one object per line. Pull: POST /api/connectors/:name/pull fetches a configured URL on demand. Live feed out: every asserted fact is published to GET /api/stream — useful to confirm a trigger fired. The roadmap turns "push to the events endpoint when something changes" into the engine doing it for you: schedules and change-data-capture sources you declare once, fail-safe and de-duplicated, so each external change is digested exactly into memory without a glue script. Related API: /api/digest — the manual ingestion you have today API: /api/connectors — webhook + poll source registration Roadmap: Durable Workflows & DAGs — what runs after a trigger fires ### Source-Available Components (roadmap) Source-Available Components TL;DR Use this page when someone asks "can I see the source?" or "is abagraph open?" and you need the accurate, current answer. Roadmap — not yet generally available. Today: abagraph is a proprietary, fully managed, hosted service . The code is closed. Plan: make select components source-available once the product is fully ready. No dates. No specific license is being promised. How you build now: entirely against the public REST and MCP APIs — the same surface that won't change when components open. Current status abagraph is the proprietary system-of-record for AI-agent memory — a bitemporal graph and a vector store in one managed engine, delivered as a hosted service with early access. Source-available means a component's source is published for reading and inspection (distinct from any particular license grant). None of it is published today; doing so for selected components is a roadmap intention, not a commitment to a date or to a named license. What this means for you Question Answer today Is there a public source repository? No. Will some components become source-available? That is the roadmap, once the product is fully ready. When, and under what license? Undecided. No dates, no license named. How do I integrate in the meantime? Through the public REST API and the MCP server — both stable and documented. Build against the API, not the internals Because integration is through the documented public surface, your code is unaffected by whether internals are open. A request you write today keeps working regardless: curl -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"subject":"Person:sarah","predicate":"works_at","object":"Org:acme","confidence":0.9,"source":"onboarding-form"}' \ "$ABAGRAPH_BASE_URL/api/facts" That fact — Person:sarah --works_at-- Org:acme with its valid_from , recorded_at , confidence , and source — behaves identically whether the engine behind it is closed or, later, partly source-available. The contract is the API, and the API is what we keep stable. Related API: /api/facts — the public surface you build against Roadmap: Customer-Defined Governance Roadmap: Durable Workflows & DAGs ### Agent Memory (sdk) Agent Memory TL;DR Import from @abagraph/client/agent , not the root package. loadContext is called once per turn and returns a frozen snapshot — facts written during the same turn are not visible until the next turn. remember returns { persisted: true, inContext: false, ids } — the inContext: false is intentional and constant. turn(task, fn) chains load → run → remember in one call. Overview createAgentMemory is a per-agent wrapper that adds three things to the base client: Frozen-snapshot semantics — context is assembled once at the start of a session and does not mutate during the turn. Any facts written via remember or digest are persisted to the server but will only appear in a future call to loadContext . This prevents mid-turn hallucination caused by partial writes leaking back into the context window. Structured logging — every operation emits a single LogEntry line to the optional onLog callback. One line per operation, with latency, token counts, and a boolean ok flag. Feedback fields — RememberInput extends FactInput with reason , why , and how fields. These are folded into properties before the fact is persisted, giving the graph a self-documenting audit trail. Import import { createAgentMemory } from "@abagraph/client/agent"; import type { AgentMemoryOptions, AgentMemory, RememberInput, RememberResult, LoadContextOpts, LogEntry, } from "@abagraph/client/agent"; createAgentMemory function createAgentMemory(opts: AgentMemoryOptions): AgentMemory AgentMemoryOptions Extends ClientOptions (from @abagraph/client ): Field Type Required Description baseUrl string Yes Base URL of the abagraph instance. apiKey string No Bearer token. Omit when auth is disabled. namespace string Yes Tenant namespace. Facts are scoped to this tenant on read and write. agent string Yes Agent identifier, e.g. "reels-bot" . Used as a seed entity in every loadContext call and as the default source prefix. source string No Provenance label for written facts. Defaults to agent:<agent> . defaultBudget number No Default token budget passed to context() and compact() . Defaults to 4096 . onLog (e: LogEntry) => void No Callback fired after every operation with a structured log entry. loadContext loadContext(task: string, opts?: LoadContextOpts): Promise<ContextPacket> Calls POST /api/context with the task as goal. Automatically adds agent:<agent> to the seeds list so the context is always anchored to the current agent's entity. Returns a frozen snapshot — this is the only memory the agent sees for the current turn. LoadContextOpts Field Type Description seeds string[] Additional entity references to anchor the graph walk. The agent entity is always included. budget number Token budget. Overrides defaultBudget for this call. maxFacts number Maximum facts to include in the packet. asOf string | number ISO-8601 timestamp or Unix seconds. Load context as it existed at this point in time. Frozen-snapshot semantics. The packet returned by loadContext is a point-in-time snapshot. Calls to remember or digest made after this snapshot will not appear in it. They will be visible in the next call to loadContext . This is intentional — it prevents the agent from reasoning about its own in-flight writes. const mem = createAgentMemory({ baseUrl: process.env.ABAGRAPH_BASE_URL ?? "https://your-tenant.abagraph.com", apiKey: process.env.ABAGRAPH_API_KEY, namespace: "acme", agent: "support-bot", defaultBudget: 4096, onLog: (e) = console.log(JSON.stringify(e)), }); const ctx = await mem.loadContext("Handle refund request for order #789", { seeds: ["Order:789"], budget: 2048, }); // ctx.summary, ctx.facts, ctx.tokens, ctx.total_tokens remember remember(facts: RememberInput[]): Promise<RememberResult> // RememberResult: { persisted: true; inContext: false; ids: string[] } Persists one or more facts. Under the hood this calls POST /api/digest in pre-extracted mode. The return value always has inContext: false — this is a deliberate reminder that the written facts will not appear in the current session's context snapshot. RememberInput Extends FactInput with three optional feedback fields that are folded into properties before persisting: Field Type Description reason string Why this fact was recorded. why string Alternate feedback field folded into properties.why . how string How the fact was derived or inferred. const result = await mem.remember([ { subject: "Order:789", predicate: "status", object: "refunded", source: "support-bot", reason: "Customer provided valid receipt, policy allows 30-day refund", how: "Matched refund-eligibility rule R42", }, ]); console.log(result.persisted, result.inContext); // true false console.log(result.ids); // ["01J..."] digest digest(input: { content: string; source?: string; contentType?: string; }): Promise<DigestResult> Fire-and-forget ingestion of raw text. Internally calls POST /api/digest in raw content mode. The server chunks, extracts facts via a BYOK model, applies PII masking, and asserts. Use when you have an unstructured transcript or document to ingest after a session. // Non-blocking: caller continues immediately; server ingests async mem.digest({ content: sessionTranscript, source: "support-session-789", contentType: "text/plain", }).catch((e) = console.error("digest failed", e)); compact compact(facts: Fact[], budget?: number): Promise<CompactResult> Confidence-ranks and trims a list of facts to fit a token budget. Uses defaultBudget when budget is omitted. const { facts, tokens, dropped } = await mem.compact(ctx.facts ?? [], 1024); enrich enrich(facts: FactInput[]): Promise<EnrichResult> PII-masks and conflict-flags a list of facts without writing them. Use as a pre-write review step when the agent produces facts that may contain sensitive data. const { facts, conflicts, masked } = await mem.enrich([ { subject: "Customer:jane", predicate: "email", object: "jane@example.com" }, ]); consolidate consolidate(): Promise<ConsolidateResult> Deduplicates persisted facts and surfaces contradictions. Run periodically — not on every turn. turn turn<T>( task: string, fn: (ctx: ContextPacket) => Promise<{ result: T; facts?: RememberInput[] }>, ): Promise<T> Convenience wrapper that sequences load → run → remember in a single call: Calls loadContext(task) to get the frozen snapshot. Passes it to fn . If fn returns facts , calls remember(facts) automatically. Returns result . const answer = await mem.turn( "What is Alice's current team?", async (ctx) = { // Your LLM call here — ctx is the frozen snapshot const result = await callLLM(ctx.summary, ctx.facts); return { result, facts: [ { subject: "Person:alice", predicate: "queried_by", object: "support-bot", reason: "User asked about team membership", }, ], }; }, ); console.log(answer); LogEntry shape Each onLog call receives one structured entry: interface LogEntry { op: string; // "loadContext" | "remember" | "digest" | "compact" | "enrich" | "consolidate" namespace: string; agent: string; ms: number; // wall-clock latency in milliseconds facts?: number; // number of facts involved (where applicable) tokens?: number; // tokens used (where applicable) total_tokens?: number; dropped?: number; // facts dropped by compact (where applicable) ok: boolean; // false if the operation threw } Production example import { createAgentMemory } from "@abagraph/client/agent"; const mem = createAgentMemory({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY!, namespace: process.env.ABAGRAPH_NAMESPACE ?? "prod", agent: "support-bot-v2", defaultBudget: 4096, onLog: (e) = { if (!e.ok) console.error("[abagraph]", e); else console.log("[abagraph]", e.op, e.ms + "ms"); }, }); // In your request handler export async function handleSupportRequest(orderId: string, userMessage: string) { return mem.turn(`Handle: ${userMessage}`, async (ctx) = { // ctx.facts is the frozen snapshot — run your LLM here const llmResponse = await callLLM({ context: ctx, message: userMessage }); return { result: llmResponse.answer, facts: llmResponse.extractedFacts?.map((f) = ({ ...f, reason: "Extracted from support conversation", })), }; }); } Next steps Memory Plugin — runner-level ADK hook built on top of this module Client Reference — underlying REST methods Types and Errors — all interfaces ### Client Reference (sdk) Client Reference TL;DR createClient({ baseUrl, apiKey? }) returns an AbagraphClient . Each method maps 1:1 to a REST endpoint. Errors throw AbagraphError . Branch-aware methods accept { branch?: string } as a second argument. createClient import { createClient } from "@abagraph/client"; function createClient(opts: ClientOptions): AbagraphClient Creates and returns a client bound to a single abagraph instance. The client is stateless and safe to share across concurrent calls. ClientOptions Field Type Required Description baseUrl string Yes Base URL of the abagraph instance, e.g. https://nas.abagraph.com . Trailing slashes are stripped automatically. apiKey string No Sent as Authorization: Bearer <apiKey> . Omit when auth is disabled. health health(): Promise<HealthResponse> // HealthResponse: { ok: boolean; [key: string]: unknown } Calls GET /api/health . Public — no authentication required. Use as a liveness probe. const { ok } = await aba.health(); console.log(ok); // true assert assert(fact: FactInput, opts?: BranchOption): Promise<Fact> Calls POST /api/facts . Writes a single fact. If a conflicting active fact already exists for the same subject and predicate (and the conflict policy is AutoSupersede , which is the default), the old fact is atomically closed and the new one is opened in a single batch. FactInput fields Field Type Required Description subject string Yes Entity reference in Type:id form, e.g. Person:alice . predicate string Yes Relationship or attribute name, e.g. works_at . object string Yes Value or entity reference, e.g. Company:acme or a plain string. confidence number No 0–1. Below the server threshold (default 0.75) the fact lands as Candidate and never supersedes. Default: 1.0. source string No Provenance label, e.g. "onboarding-form" . properties Record<string, unknown> No Arbitrary JSON metadata attached to the fact. policy string No Conflict policy override: "AutoSupersede" (default), "Coexist" (set-valued predicates), or "Reject" . valid_until string No ISO-8601 timestamp at which the fact expires (closes its validity window). Omit for an open-ended fact. transient boolean No If true , the fact is not persisted to the write-ahead log (in-memory only). Default: false . opts.branch — optional branch name. The fact is written into that branch's isolated overlay. const fact = await aba.assert({ subject: "Person:alice", predicate: "works_at", object: "Company:acme", confidence: 0.95, source: "onboarding-form", }); console.log(fact.status); // "Active" // Set-valued predicate: use Coexist so both tags stay active await aba.assert({ subject: "Person:alice", predicate: "tag", object: "engineer", policy: "Coexist", }); // Branch-isolated write await aba.assert( { subject: "Person:alice", predicate: "works_at", object: "Company:beta" }, { branch: "experiment-1" }, ); retract retract(id: string): Promise<DeleteResult> // DeleteResult: { ok: boolean; id: string; [key: string]: unknown } Calls DELETE /api/facts/:id . Marks the fact as Retracted . Retracted facts are hidden from normal queries but remain in the write-ahead log and are visible via as_of . const { ok, id } = await aba.retract("01J..."); console.log(ok, id); query query(params?: QueryParams): Promise<Fact[]> Calls GET /api/facts . Returns facts matching the supplied filters. All filters are optional and AND-ed together. QueryParams fields Field Type Description subject string Filter by subject entity reference. predicate string Filter by predicate name. object string Filter by object value or entity reference. status string Filter by fact status: Active , Candidate , Superseded , or Retracted . limit number Maximum results to return. Server default: 200. as_of string | number ISO-8601 timestamp or Unix seconds. Returns the historical world-state at that point in time. branch string Read from the named branch overlay. // All active facts for alice const facts = await aba.query({ subject: "Person:alice" }); // Historical snapshot: what was true at a past timestamp? const past = await aba.query({ subject: "Person:alice", predicate: "works_at", as_of: "2025-01-01T00:00:00Z", }); // Read from a branch const draft = await aba.query({ branch: "experiment-1" }); transact transact(body: TransactBody, opts?: BranchOption): Promise<TransactResult> Calls POST /api/transact . Atomic compare-and-swap batch: compares are evaluated first; any mismatch aborts the entire transaction with 409 CompareFailed . TransactBody fields Field Type Description asserts FactInput[] Required. Facts to assert atomically. compares unknown[] Optional. Preconditions that must hold (current value checks). If any fail, the transaction is aborted with CompareFailed . retracts string[] Optional. Fact IDs to retract as part of the same batch. const result = await aba.transact({ asserts: [ { subject: "Person:alice", predicate: "works_at", object: "Company:beta" }, { subject: "Person:alice", predicate: "title", object: "Staff Engineer" }, ], retracts: ["01J-old-fact-id"], }); console.log(result.tx, result.facts.length); context context(req: ContextRequest): Promise<ContextPacket> Calls POST /api/context . The flagship one-call context assembly: given a goal and optional seed entities, the server assembles a confidence-ranked, token-bounded context packet combining semantic recall, graph walk, corroboration, and optional summarisation. ContextRequest fields Field Type Description goal string Required. Natural-language task description used for semantic recall. seeds string[] Optional. Entity references to anchor the graph walk (e.g. ["Person:alice"] ). max_facts number Optional. Maximum facts to include. Default: 32. max_pages number Optional. Maximum wiki pages to include. as_of string | number Optional. Time-travel: assemble context as it existed at this timestamp. token_budget number Optional. Confidence-rank and truncate to this many tokens before returning. project string Optional. Namespace / project scope for multi-tenant deployments. const ctx = await aba.context({ goal: "What is Alice's current role and team?", seeds: ["Person:alice"], token_budget: 4096, max_facts: 32, }); // ctx.summary, ctx.facts, ctx.entities, ctx.tokens, ctx.total_tokens search search(q: string): Promise<SearchResult> Calls POST /api/search . Semantic kNN search: embeds the question and retrieves nearest-neighbour facts. Returns { answer_facts, entities } . const result = await aba.search("Who does Alice report to?"); // result.answer_facts, result.entities graph graph(): Promise<GraphResult> Calls GET /api/graph . Returns the full graph as { nodes, edges, total_nodes, total_edges } . walk walk(params: WalkParams): Promise<GraphResult> Calls GET /api/walk . Breadth-first traversal from a start entity. WalkParams fields Field Type Description start string Required. Starting entity reference, e.g. Person:alice . depth number Optional. Max traversal depth. direction string Optional. "outbound" , "inbound" , or "both" . edge_types string Optional. Comma-separated predicate names to follow. as_of string | number Optional. Walk the historical graph at this timestamp. const { reached, edges } = await aba.walk({ start: "Person:alice", depth: 3, direction: "outbound", edge_types: "works_at,reports_to", }); factcheck factcheck(params?: FactCheckParams): Promise<FactCheckResult> Calls GET /api/factcheck . Surfaces active contradictions: facts with the same subject and predicate that should have been superseded but were asserted with Coexist or are otherwise inconsistent. const { contradictions, count } = await aba.factcheck({ subject: "Person:alice" }); connect connect(): Promise<ConnectInfo> Calls GET /api/connect . Returns connection metadata and ready-to-paste code snippets for the current tenant. The server never includes real API keys in the response — the token field uses the placeholder <YOUR_API_KEY> . const info = await aba.connect(); console.log(info.rest_base, info.mcp_endpoint); console.log(info.snippets?.ts); // ready-to-paste TypeScript digest digest(req: DigestRequest): Promise<DigestResult> Calls POST /api/digest . Two modes — pass exactly one: Raw content mode : { content, content_type?, source? } — the server chunks, extracts facts via a BYOK model, judges confidence, applies PII masking, and asserts. Pre-extracted mode : { facts: FactInput[], source? } — bulk-asserts facts you already extracted. // Raw content await aba.digest({ content: "Alice joined Acme in January 2024 as Staff Engineer.", content_type: "text/plain", source: "hr-doc-42", }); // Pre-extracted facts await aba.digest({ facts: [ { subject: "Person:alice", predicate: "joined", object: "2024-01" }, ], source: "hr-doc-42", }); compact compact(req: { facts: Fact[]; token_budget: number }): Promise<CompactResult> // CompactResult: { facts: Fact[]; tokens: number; total_tokens: number; dropped: number } Calls POST /api/compact . Confidence-ranks a supplied list of facts and trims to token_budget , returning the survivors and drop count. Use after context() when you need a tighter fit for a prompt window. const { facts, tokens, dropped } = await aba.compact({ facts: ctx.facts ?? [], token_budget: 1024, }); enrich enrich(req: { facts: FactInput[]; mask?: "redact" | "hash" | "shift_date"; }): Promise<EnrichResult> Calls POST /api/enrich . PII-masks and conflict-flags a list of facts without writing them. Use as a pre-write review step. const { facts, conflicts, masked } = await aba.enrich({ facts: [{ subject: "Person:bob", predicate: "email", object: "bob@example.com" }], mask: "redact", }); consolidate consolidate(): Promise<ConsolidateResult> // ConsolidateResult: { collapsed: number; superseded: string[]; conflicts: number } Calls POST /api/consolidate . Deduplicates persisted facts and surfaces contradictions in one pass. Run periodically to keep the graph clean. const { collapsed, superseded, conflicts } = await aba.consolidate(); tenants (admin) The tenants property groups tenant lifecycle methods. Requires admin credentials. // Create a tenant const tenant = await aba.tenants.create({ id: "acme", name: "Acme Corp" }); // List all tenants const list = await aba.tenants.list(); // Get one tenant const t = await aba.tenants.get("acme"); // Delete a tenant await aba.tenants.delete("acme"); branches The branches property groups branch lifecycle methods. Branches are instant forks of the graph — write-isolated overlays that can be diffed, promoted to main , or discarded. // Create a branch (optionally pin it to a past timestamp) const branch = await aba.branches.create({ name: "experiment-1", as_of: "2025-06-01T00:00:00Z", }); // List branches const list = await aba.branches.list(); // Get one branch const b = await aba.branches.get("experiment-1"); // Diff branch vs main const diff = await aba.branches.diff("experiment-1"); // Promote changes back to main await aba.branches.promote("experiment-1"); // Discard await aba.branches.discard("experiment-1"); Next steps SDK Overview — package setup and authentication Agent Memory — createAgentMemory for LLM loops Types and Errors — full interface and error-code reference API Reference: /api/context — server-side context assembly details ### SDK Overview (sdk) SDK Overview TL;DR Install: npm install @abagraph/client Three entry points: @abagraph/client , @abagraph/client/agent , @abagraph/client/plugin . Zero dependencies. Requires Node 18 or later (uses the built-in global fetch ). What is the SDK @abagraph/client (version 0.1.0) is the official TypeScript client for abagraph — the bitemporal graph and vector memory engine for LLM agents. It is a thin, typed wrapper over the abagraph REST API: no bundled runtime, no transitive packages, no code generation. Three independently importable entry points match the three use-cases: @abagraph/client — raw REST client for any Node.js or edge environment. @abagraph/client/agent — per-agent memory wrapper with frozen-snapshot semantics, suited for LLM loops. @abagraph/client/plugin — ADK-style runner-level hook; install once, every agent gets memory automatically. Requirements Node.js 18 or later (uses the built-in fetch global — no polyfill required). An abagraph API endpoint — your base URL and API key are shown in the abagraph console under Connect . An API key ( ABAGRAPH_API_KEY ) if your instance has authentication enabled. Install npm install @abagraph/client # or pnpm add @abagraph/client Quick start Assert a fact, query it back, then fetch goal-directed context in three calls: import { createClient } from "@abagraph/client"; const aba = createClient({ baseUrl: process.env.ABAGRAPH_BASE_URL ?? "https://your-tenant.abagraph.com", apiKey: process.env.ABAGRAPH_API_KEY, }); // 1. Write a fact const fact = await aba.assert({ subject: "Person:alice", predicate: "works_at", object: "Company:acme", confidence: 0.95, source: "onboarding-form", }); console.log(fact.id, fact.status); // "01J..." "Active" // 2. Query it back const facts = await aba.query({ subject: "Person:alice" }); console.log(facts[0].object); // "Company:acme" // 3. Retrieve goal-directed context for an agent turn const ctx = await aba.context({ goal: "Summarise Alice's current role", seeds: ["Person:alice"], token_budget: 2048, }); console.log(ctx.summary); Entry points @abagraph/client Exports createClient , all domain interfaces, and AbagraphError . This is the foundation every other entry point builds on. import { createClient, AbagraphError } from "@abagraph/client"; import type { Fact, FactInput, ContextRequest } from "@abagraph/client"; @abagraph/client/agent Exports createAgentMemory . Use this when you control the per-agent loop and want frozen-snapshot memory: context is loaded once at the start of a turn and does not include facts written during that same turn. import { createAgentMemory } from "@abagraph/client/agent"; import type { AgentMemoryOptions, RememberInput } from "@abagraph/client/agent"; @abagraph/client/plugin Exports memoryPlugin . Use this at the runner level — once per process — so every agent session automatically loads context at start and persists output at end. import { memoryPlugin } from "@abagraph/client/plugin"; import type { MemoryPlugin, SessionStartCtx, SessionEndCtx } from "@abagraph/client/plugin"; Authentication Pass apiKey to createClient . The SDK sends it as Authorization: Bearer <apiKey> on every request. If the server has authentication disabled (development default), omit the field — the SDK works without it. const aba = createClient({ baseUrl: "https://your-tenant.abagraph.com", apiKey: process.env.ABAGRAPH_API_KEY, }); Never hard-code API keys. Always read them from environment variables or a secrets manager. The server never echoes your real token in any response, including GET /api/connect . Error handling Every non-2xx response throws AbagraphError . Catch it and inspect error.status (HTTP status code) and error.code (machine-readable server code such as Conflict , NotFound , or CompareFailed ). import { createClient, AbagraphError } from "@abagraph/client"; try { await aba.assert({ subject: "...", predicate: "...", object: "..." }); } catch (e) { if (e instanceof AbagraphError) { console.error(e.code, e.status, e.message); } } See Types and Errors for the full error-code table. Next steps Client Reference — every createClient method with signatures and examples Agent Memory — frozen-snapshot memory for LLM loops Memory Plugin — runner-level ADK hook Types and Errors — all interfaces and AbagraphError ### Memory Plugin (sdk) Memory Plugin TL;DR Import from @abagraph/client/plugin . Create one instance per runner process — not per agent or per request. onSessionStart(ctx) loads and freezes context; await it before running the agent. onSessionEnd(ctx) is fire-and-forget — do not await it. Overview memoryPlugin wraps createAgentMemory in an ADK-compatible two-hook interface ( onSessionStart / onSessionEnd ). It is designed to be installed once at the runner level so that every agent session automatically gains persistent memory without requiring any per-agent changes. The two hooks map to the natural lifecycle of an agent session: onSessionStart — called before the agent runs. Loads the context snapshot and returns it. The agent receives only this frozen packet for the duration of the session. onSessionEnd — called after the agent finishes. Persists the session output (raw transcript or pre-extracted facts). Returns immediately; all I/O is fire-and-forget so it never blocks the response path. Import import { memoryPlugin } from "@abagraph/client/plugin"; import type { MemoryPlugin, SessionStartCtx, SessionEndCtx, } from "@abagraph/client/plugin"; memoryPlugin function memoryPlugin(opts: AgentMemoryOptions): MemoryPlugin Accepts the same AgentMemoryOptions as createAgentMemory . Returns a MemoryPlugin with two hooks. AgentMemoryOptions (recap) Field Type Required Description baseUrl string Yes Base URL of the abagraph instance. apiKey string No Bearer token. namespace string Yes Tenant namespace. agent string Yes Agent identifier, e.g. "reels-bot" . source string No Provenance label. Defaults to agent:<agent> . defaultBudget number No Default token budget. Defaults to 4096 . onLog (e: LogEntry) => void No Structured log callback. onSessionStart onSessionStart(ctx: SessionStartCtx): Promise<ContextPacket> Calls loadContext internally. Returns the frozen context snapshot for the session. Await this before running your agent — the returned packet is the only memory the agent will see during this session. SessionStartCtx Field Type Required Description task string Yes Natural-language task description. Used as the goal for context assembly. seeds string[] No Additional entity references to anchor the graph walk. budget number No Token budget for this session. Overrides defaultBudget . maxFacts number No Maximum facts to include in the packet. asOf string | number No ISO-8601 timestamp or Unix seconds. Load context as it existed at this point in time. onSessionEnd onSessionEnd(ctx: SessionEndCtx): void Persists session output. Returns void synchronously — all writes are fire-and-forget. Do not await this method. Pass either content (raw transcript) or facts (pre-extracted), not both. If content is provided, calls digest({ content }) (raw ingestion path). If facts is provided (and content is absent), calls remember(facts) . If neither is provided, no write is performed. SessionEndCtx Field Type Required Description content string No Raw transcript or session text to digest. Mutually exclusive with facts . facts RememberInput[] No Pre-extracted facts to remember directly. Used when content is not available. source string No Provenance label for written facts. Install at the runner Create the plugin once, outside any request or session handler: import { memoryPlugin } from "@abagraph/client/plugin"; // Created once — shared across all sessions const memory = memoryPlugin({ baseUrl: process.env.ABAGRAPH_BASE_URL!, apiKey: process.env.ABAGRAPH_API_KEY!, namespace: process.env.ABAGRAPH_NAMESPACE ?? "prod", agent: "reels-bot", defaultBudget: 4096, onLog: (e) = console.log("[abagraph]", JSON.stringify(e)), }); Per-session usage // In your session handler (called once per user session) async function runSession(userMessage: string) { // 1. Load and freeze context — await this const ctx = await memory.onSessionStart({ task: userMessage, budget: 2048, }); // 2. Run your agent with the frozen context const { answer, transcript } = await runAgent({ context: ctx, message: userMessage }); // 3. Persist session output — do NOT await memory.onSessionEnd({ content: transcript }); return answer; } Do not await onSessionEnd. The method is intentionally synchronous in return type. Awaiting it would block the response path. Errors in the write-back are silently swallowed — if you need write-back failure visibility, use the onLog callback with ok: false entries. Pre-extracted facts at session end When you already have structured facts (e.g. from a structured LLM output), pass them directly instead of a raw transcript: memory.onSessionEnd({ facts: [ { subject: "Customer:jane", predicate: "preference", object: "dark mode", reason: "Stated preference during onboarding", }, ], }); Historical context with asOf To give an agent access to how the graph looked at a past point in time (for example, for audit replay or regression testing), pass asOf to onSessionStart : const ctx = await memory.onSessionStart({ task: "Audit refund decision for order #789", asOf: "2025-03-01T00:00:00Z", }); Difference from createAgentMemory memoryPlugin is a thin runner-level adapter over createAgentMemory . Use createAgentMemory directly when you need: Fine-grained control over individual operations ( compact , enrich , consolidate ). The turn convenience method. A per-agent (not per-runner) instance with distinct options. Use memoryPlugin when you own the runner and want to install memory once without touching individual agent code. Next steps Agent Memory — the underlying module with full method control Client Reference — base REST client Types and Errors — all interfaces ### Types and Errors (sdk) Types and Errors TL;DR All types are re-exported from @abagraph/client (core types), @abagraph/client/agent (agent types), and @abagraph/client/plugin (plugin types). AbagraphError is thrown for every non-2xx response. Check error.code for the machine-readable server code and error.status for the HTTP status. All interfaces are intentionally loose ( [key: string]: unknown index signature on response types) for forward compatibility. Importing types // Core client types import type { ClientOptions, BranchOption, Fact, FactInput, QueryParams, TransactBody, TransactResult, ContextRequest, ContextPacket, DigestRequest, DigestResult, CompactResult, EnrichConflict, EnrichResult, ConsolidateResult, WalkParams, Tenant, TenantInput, Branch, BranchInput, BranchDiff, BranchApi, TenantApi, FactCheckParams, FactCheckResult, ConnectInfo, HealthResponse, DeleteResult, OkResult, GraphResult, SearchResult, AbagraphClient, } from "@abagraph/client"; // Agent types import type { AgentMemoryOptions, AgentMemory, LogEntry, RememberInput, RememberResult, LoadContextOpts, } from "@abagraph/client/agent"; // Plugin types import type { MemoryPlugin, SessionStartCtx, SessionEndCtx, } from "@abagraph/client/plugin"; AbagraphError class AbagraphError extends Error { readonly code?: string; // Machine-readable code from the server error envelope readonly status: number; // HTTP status code (e.g. 404, 409, 500) readonly name: string; // Always "AbagraphError" } Thrown for every non-2xx HTTP response. The message field carries the human-readable server message. Use code for programmatic branching. import { createClient, AbagraphError } from "@abagraph/client"; try { await aba.assert({ subject: "X:1", predicate: "y", object: "z" }); } catch (e) { if (e instanceof AbagraphError) { switch (e.code) { case "Conflict": // 409 — a compare-guarded transact found a mismatch break; case "NotFound": // 404 break; case "CompareFailed": // 409 — transact precondition failed; server includes a `failures` array break; default: console.error(e.status, e.message); } } } Server error codes The server returns { "error": { "code": "...", "message": "..." } } for all errors. Known codes: Code HTTP status Description NotFound 404 The requested resource (fact, tenant, branch) does not exist. Conflict 409 A write conflicted with an existing fact under the Reject conflict policy. CompareFailed 409 A transact compare precondition failed. The response body includes a failures array listing the mismatched checks. SchemaInvalid 400 The request body failed schema validation (missing required field, wrong type). StorageCorruption 500 The storage backend detected an unrecoverable data inconsistency. Io 500 A disk or network I/O error occurred in the storage layer. Closed 503 The engine is shut down (graceful drain in progress). MethodNotAllowed 405 The HTTP method is not supported on this endpoint. Core types Fact interface Fact { id: string; subject: string; // "Type:id" entity reference predicate: string; object: string; // value or "Type:id" entity reference confidence?: number; // 0–1 source?: string; status?: string; // "Active" | "Candidate" | "Superseded" | "Retracted" properties?: Record<string, unknown>; valid_from?: string; // ISO-8601 timestamp valid_until?: string | null; // null = open-ended tx?: number; // write transaction ID [key: string]: unknown; } FactInput interface FactInput { subject: string; predicate: string; object: string; confidence?: number; source?: string; properties?: Record<string, unknown>; policy?: string; // "AutoSupersede" | "Coexist" | "Reject" valid_until?: string; // ISO-8601 transient?: boolean; } QueryParams interface QueryParams { subject?: string; predicate?: string; object?: string; status?: string; // "Active" | "Candidate" | "Superseded" | "Retracted" limit?: number; // default 200 on the server as_of?: string | number; // ISO-8601 or Unix seconds branch?: string; } TransactBody interface TransactBody { compares?: unknown[]; // preconditions; any failure aborts the transaction asserts: FactInput[]; retracts?: string[]; // fact IDs to retract atomically } interface TransactResult { tx: number; facts: Fact[]; retracted: string[]; [key: string]: unknown; } ContextRequest interface ContextRequest { goal: string; project?: string; seeds?: string[]; // entity references to anchor the graph walk max_facts?: number; // default 32 max_pages?: number; as_of?: string | number; token_budget?: number; // confidence-rank and trim to this many tokens } DigestRequest // Two modes — pass one, not both: type DigestRequest = | { content: string; content_type?: string; source?: string } // raw ingestion | { facts: FactInput[]; source?: string }; // pre-extracted interface DigestResult { written: number; written_ids?: string[]; superseded?: number; candidates?: number; entities?: string[]; } CompactResult interface CompactResult { facts: Fact[]; tokens: number; total_tokens: number; dropped: number; } EnrichResult interface EnrichConflict { subject: string; predicate: string; current: string; incoming: string; } interface EnrichResult { facts: FactInput[]; masked: number; conflicts: EnrichConflict[]; } ConsolidateResult interface ConsolidateResult { collapsed: number; superseded: string[]; conflicts: number; } WalkParams interface WalkParams { start: string; // starting entity reference depth?: number; direction?: string; // "outbound" | "inbound" | "both" edge_types?: string; // comma-separated predicate names as_of?: string | number; } Branch and BranchInput interface Branch { name: string; parent?: string; as_of?: string | number | null; [key: string]: unknown; } interface BranchInput { name: string; parent?: string; as_of?: string | number; } Tenant and TenantInput interface Tenant { id: string; name?: string; quota_facts?: number; [key: string]: unknown; } interface TenantInput { id: string; name?: string; quota_facts?: number; } FactCheckParams and FactCheckResult interface FactCheckParams { subject?: string; predicate?: string; } interface FactCheckResult { contradictions: unknown[]; count: number; [key: string]: unknown; } ConnectInfo interface ConnectInfo { tenant?: string; rest_base?: string; mcp_endpoint?: string; stream_endpoint?: string; snippets?: Record<string, unknown>; [key: string]: unknown; } Agent types AgentMemoryOptions interface AgentMemoryOptions extends ClientOptions { namespace: string; agent: string; source?: string; // default: "agent:<agent>" defaultBudget?: number; // default: 4096 onLog?: (e: LogEntry) => void; } LogEntry interface LogEntry { op: string; // operation name namespace: string; agent: string; ms: number; // latency in milliseconds facts?: number; tokens?: number; total_tokens?: number; dropped?: number; ok: boolean; } RememberInput and RememberResult // Extends FactInput with optional feedback fields folded into properties interface RememberInput extends FactInput { reason?: string; // why this fact was recorded why?: string; // alternate feedback field how?: string; // how the fact was derived } interface RememberResult { persisted: true; // always true on success inContext: false; // always false — frozen-snapshot semantics ids: string[]; // IDs of the persisted facts } LoadContextOpts interface LoadContextOpts { seeds?: string[]; budget?: number; maxFacts?: number; asOf?: string | number; } Plugin types SessionStartCtx interface SessionStartCtx { task: string; seeds?: string[]; budget?: number; maxFacts?: number; asOf?: string | number; } SessionEndCtx interface SessionEndCtx { content?: string; // raw transcript — mutually exclusive with facts facts?: RememberInput[]; // pre-extracted facts — mutually exclusive with content source?: string; } MemoryPlugin interface MemoryPlugin { onSessionStart(ctx: SessionStartCtx): Promise<ContextPacket>; onSessionEnd(ctx: SessionEndCtx): void; // fire-and-forget, never await } Next steps SDK Overview — package setup and authentication Client Reference — all AbagraphClient methods Agent Memory — createAgentMemory details Memory Plugin — memoryPlugin runner hook ### Authentication and Authorization (security) Authentication and Authorization TL;DR Use this page when you are wiring a client to the API and need to know what key to send, where it works, and what a rejection looks like. Outcome first: every request resolves to exactly one identity — Admin (operator, cross-namespace) or Tenant(id) (your namespace only) — and an unauthorized call gets 404 , not 401 , so no gated surface is advertised. Send your tenant key as Authorization: Bearer <key> , or log a browser in once for an abagraph_session cookie that carries the same key. A tenant key is bound to one namespace and one subdomain; using it elsewhere is rejected. Two kinds of key The hosted service issues credentials, you do not configure them yourself. There are two kinds: Identity Who holds it Effect on a request Tenant(id) You, the customer Reads and writes are scoped to your namespace. Bound to one subdomain (see subdomain enforcement ). Admin The platform operator Cross-namespace access. Not issued to customers. For almost everything you build, you hold a tenant key . Its namespace is fixed by the key — you never name a namespace in a request, and a body that tries to is overwritten with the key's namespace. How a request is authenticated For every inbound request the service resolves the caller in this order: Authorization header — if Authorization: Bearer <key> is present, the key is matched to a tenant (or the operator admin key). Session cookie — if there is no Authorization header, the abagraph_session cookie is read and matched the same way. No credential — the request is denied. The resolved identity then governs every read and write for that request: a Tenant(id) caller sees only its namespace; a denied caller sees nothing. Sending a bearer key curl https://acme.abagraph.com/api/facts \ -H "Authorization: Bearer tok-acme" Cookie sessions Browsers can log in once instead of carrying a header on every request. The login endpoint exchanges your key for an abagraph_session cookie: # Log in — sets the abagraph_session cookie curl -X POST https://acme.abagraph.com/api/session \ -H "Content-Type: application/json" \ -d '{"key": "tok-acme"}' # Log out — clears the cookie curl -X DELETE https://acme.abagraph.com/api/session Success response: {"scope": "tenant", "tenant": "acme"} The cookie is set HttpOnly; Secure; SameSite=Lax; Path=/ with a 30-day max-age. It carries the key itself, and the key is re-validated on every subsequent request — so when a key is revoked, every outstanding session for it stops working immediately. The cookie is not a signed token. It carries your key, so treat it like one: serve only over HTTPS and rely on key revocation to invalidate sessions. Subdomain enforcement Each tenant has its own subdomain (e.g. acme.abagraph.com ). The subdomain only routes the request; the caller still authenticates with their own key. The binding rules: A tenant key on its own subdomain: allowed . A tenant key on a different subdomain: rejected (404) — the URL cannot be used as a cross-tenant read vector. www , multi-label hosts like a.b.abagraph.com , and labels with non-alphanumeric characters: rejected. # Correct: key matches the subdomain curl https://acme.abagraph.com/api/facts -H "Authorization: Bearer tok-acme" # Rejected (404): tenant key on the wrong subdomain curl https://globex.abagraph.com/api/facts -H "Authorization: Bearer tok-acme" Why denials return 404, not 401 An unauthorized request receives 404 NotFound , not 401 Unauthorized . A 401 would confirm to an attacker that a protected surface exists at that path. The 404 is indistinguishable from a missing route, so a gated endpoint looks identical to one that was never there. Public endpoints A small set of paths are reachable without a key: GET /api/health — liveness probe. POST /api/signup — lead capture (writes nothing to your namespace). Discovery paths such as /api and /v1 — return a 401 challenge used by SDK auto-discovery. The UI shell on a tenant subdomain — it must load before the login form can post your key. A subdomain with no session shows a login page, never your data. What a rejection looks like Situation HTTP No key, wrong key, or key on the wrong subdomain 404 Discovery path probed without a key 401 challenge Authenticated but the resource is not in your namespace 404 (the fact is invisible to you) Define Your Own Policy — the full set of knobs you control Tenant Isolation — how the resolved identity becomes a read filter BYOK Model Keys — per-request model keys are separate from API auth ### BYOK Model Keys (security) BYOK Model Keys TL;DR Use this page when an operation needs a language model (digest, ask) and you want to know where your model key goes. Outcome first: you supply a model key per request; it is used for one outbound call and then discarded — never written to the graph, never written to disk, never printed to a log. The key enters exactly one outbound request header, and any error string is scrubbed of it before you or a log sees it. Supported providers: anthropic (default) and any openai -compatible endpoint. What BYOK means Bring-your-own-key (BYOK) means the service does not hold or proxy model credentials on your behalf. You attach a key to each request that needs a model call. The service uses it for the outbound HTTPS calls that operation requires (extract + judge for digest; one call for ask), then discards it. The key is never stored in your facts, never written to disk, and never appears in a log line. The model object You pass model credentials as a JSON model object on the request: Field Required Description provider no anthropic (default) or openai . model yes Model name, e.g. claude-opus-4-5 or gpt-4o . key yes Your API key for that provider. endpoint no Override the default endpoint (used for OpenAI-compatible servers). Missing key or model returns a SchemaInvalid (400) error before any outbound call is attempted. Default endpoints Provider Default endpoint anthropic https://api.anthropic.com openai https://api.openai.com/v1 Any OpenAI-compatible endpoint (Azure OpenAI, a local LLM server, etc.) works by setting endpoint with "provider": "openai" . Key lifecycle For one request, your key travels exactly this path: Parsed from the request body, in memory only. Placed in the outbound request header — x-api-key for Anthropic, Authorization: Bearer for OpenAI. The response is read; the outbound request and its headers are dropped. The credential goes out of scope when the request finishes. At no point is the key passed to storage, to the embedding provider, or to any telemetry sink. The only thing logged is a redacted view, with the key replaced by ***redacted*** . Error scrubbing If the outbound model call fails, the underlying error string could echo the request. Before that error is returned to you or written to a log, it is scrubbed so any occurrence of your key is replaced with *** . The raw key appears in neither place. POST /api/digest Digest takes separate extract_model and judge_model objects. Supply both, or supply a single model and it is used for both steps: curl -X POST https://acme.abagraph.com/api/digest \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "content": "Alice joined Acme in March.", "content_type": "text/plain", "source": "hr-import", "extract_model": {"provider": "anthropic", "model": "claude-opus-4-5", "key": " "}, "judge_model": {"provider": "anthropic", "model": "claude-opus-4-5", "key": " "} }' POST /api/ask Ask uses one model object for a grounded answer. Evidence is retrieved from your namespace (namespace-scoped semantic search) before the model is called, so the model only ever sees pre-retrieved, masked facts: curl -X POST https://acme.abagraph.com/api/ask \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "question": "When did Alice join Acme?", "top_k": 8, "model": {"provider": "openai", "model": "gpt-4o", "key": " "} }' OpenAI-compatible (custom endpoint) curl -X POST https://acme.abagraph.com/api/ask \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "question": "Summarise Alice'\''s work history.", "model": {"provider": "openai", "endpoint": "http://localhost:11434/v1", "model": "llama3.2", "key": "ollama"} }' Over MCP The digest MCP tool takes the same extract_model / judge_model shape. The same lifecycle holds: the key is used for the outbound model call and then discarded — it is never written to the graph the tool operates on. What is not BYOK The embedding provider used to store and search facts is not BYOK. It is configured for your hosted tenant at the platform level, not supplied per request. Even so, the embedding credential is never written into your fact data. Define Your Own Policy — BYOK among your other policy knobs PII Masking — PII is masked before facts reach the embedding provider Authentication and Authorization — model keys are separate from your API key Tenant Isolation — model evidence is namespace-scoped before the model sees it ### Define Your Own Policy (security) Define Your Own Policy TL;DR Use this page when you are about to write or read data and need to know which safety rules you control vs. which the service enforces for you. Outcome first: with the right key and the right per-assert settings, one customer can never read another's data, low-trust claims never overwrite trusted ones, and your model keys never get stored. Your policy surface today: per-tenant API keys (subdomain-bound), the namespace isolation model, the confidence threshold (≥ 0.75 Active, < 0.75 Candidate), per-assert conflict policy (functional vs. coexist), bring-your-own model keys , and partial PII masking . Programmable, customer-defined policy-as-code is on the roadmap — see Customer-Defined Governance . I want to… → do this I want to… How Why Stop any other customer reading my data Every fact carries your namespace ; your API key is pinned to it, so reads are filtered to it automatically. The namespace is the only isolation boundary. Store a claim the engine should not yet trust Assert with confidence < 0.75 → lands as Candidate . Candidates are searchable but do not supersede trusted facts. Keep several values for one predicate Assert with policy: "coexist" . Functional predicates keep only the newest value. Let a new value replace the old automatically Use the default functional behaviour ( auto_supersede ). One active object per subject+predicate. Use my own model provider and key Pass a model object per request (BYOK). The key is used once, then discarded — never stored. Strip emails / SSNs before storage Digest with a mask strategy. Masking runs before the value is embedded or persisted. The structural rule: namespace is the only chokepoint Read this once and internalise it: Every fact has a namespace. A query without a namespace filter would return ALL tenant data. The namespace is the only isolation chokepoint — reads filter on it, and conflict resolution is namespace-local. A namespace (also called the tenant) is a label stamped on every fact when it is written. Your tenant API key is bound to exactly one namespace, so the hosted service applies the filter for you on every read — you do not add it yourself. The only way to read across namespaces is with an operator-level admin key, and that path is not exposed to customers. Concrete example — the same triple in two namespaces never collide: namespace=acme Person:sarah --works_at-- Org:acme (confidence 0.95) namespace=globex Person:sarah --works_at-- Org:globex (confidence 0.95) Both stay Active . An assert in acme never closes a fact in globex , even though subject and predicate match — conflict resolution is scoped to a single namespace. Per-tenant API keys (subdomain-bound) You authenticate with a tenant API key sent as a Bearer token (or a cookie session that carries the same key). Two binding rules are enforced for you: Namespace-bound. A tenant key can only read and write its own namespace. It cannot name a different namespace, even if the request body tries to — the server overwrites the field with the key's namespace. Subdomain-bound. A tenant key works only on its own tenant subdomain. tok-acme on acme.abagraph.com is allowed; the same key on globex.abagraph.com is rejected. This stops the URL being used as a cross-tenant read vector. # Allowed: key matches the subdomain curl https://acme.abagraph.com/api/facts -H "Authorization: Bearer tok-acme" # Rejected (404): tenant key on the wrong subdomain curl https://globex.abagraph.com/api/facts -H "Authorization: Bearer tok-acme" See Authentication and Authorization for the full key model. The confidence threshold (0.75) Every fact carries a confidence between 0.0 and 1.0. One falsifiable rule decides its lifecycle: confidence ≥ 0.75 → Active . Participates in conflict resolution and can supersede an older value. confidence < 0.75 → Candidate . Stored and searchable, but does not supersede an Active fact. Reasserting the same triple at full confidence promotes it to Active. Why: a guess from a noisy source must never silently overwrite a fact you trust. Default confidence when you omit it is 1.0 . Per-assert conflict policy Conflict handling is chosen per assert , not globally. Set policy on each write: policy Behaviour Use for auto_supersede (default) The new Active fact closes the prior Active fact for that subject+predicate. Functional facts — one current value (e.g. works_at , status ). coexist The new fact is kept alongside existing ones; none is closed. Set-valued predicates (e.g. tag , references ). reject If a conflicting Active fact exists, the write fails with Conflict (409). Write-once guarantees. Why per-assert: the same predicate can be functional in one record and set-valued in another, so the decision lives on the write, not on a schema flag. Bring your own model keys (BYOK) Operations that call a language model (digest, ask) take the credential from a model object on the request — provider, model name, and key. The key is used for that one outbound call and then discarded: it is never written to the graph, never written to disk, and never appears in a log line (only a redacted view is logged). You decide which provider and which key per request. See BYOK Model Keys . PII masking — what is and isn't covered When you digest content you can pick a mask strategy ( redact , hash , or shift_date ) that runs before a value is embedded or stored. Be honest about the scope: Masked: text in the subject , text-valued objects, and string values in the properties map. Detected kinds: email, phone, SSN, credit-card number. Not masked: numeric, boolean, and blob objects; PII glued inside a longer token with no surrounding whitespace; anything detection's heuristics miss (there is no ML classifier). Treat masking as defence-in-depth, not a guarantee. If your data demands strict coverage, scrub upstream before you send it. See PII Masking for the exact detection rules. Minimal ✓ / ✗ example ✓ Assert Person:sarah --plan-- "enterprise" at confidence 0.95 → Active; supersedes the prior plan. Your tenant key pins namespace=acme, so only acme can read it. ✗ Assert Person:sarah --plan-- "enterprise" at confidence 0.4 → Candidate; the OLD plan stays Active. Low-confidence claims never overwrite. ✗ Tag with the default functional policy instead of coexist → the second tag supersedes the first, and you silently lose a tag. Pre-query checklist Am I sending my tenant API key? It pins the namespace and the subdomain for me — I never add a namespace filter by hand. Is my confidence intentional? ≥ 0.75 supersedes; < 0.75 parks as a non-superseding Candidate. For set-valued predicates, did I pass policy: "coexist" so I don't silently overwrite? If the payload may contain PII, did I choose a mask strategy — knowing numbers, blobs, and glued tokens are not covered? For a model call, did I pass my own model key (BYOK) on this request? Policy as code (roadmap) The knobs above are the shipped, per-request controls. Declarative, customer-defined policy-as-code — approval gates, retention rules, and governance you author once and apply across writes — is planned. See Customer-Defined Governance . Authentication and Authorization — the per-tenant key model Tenant Isolation — the namespace chokepoint in depth PII Masking — detection rules and limits BYOK Model Keys — per-request key lifecycle Customer-Defined Governance — policy-as-code on the roadmap ### PII Masking (security) PII Masking TL;DR Use this page when you are about to digest content that may contain personal data and need to know exactly what gets masked and what slips through. Outcome first: personal data is masked before a value is sent to the embedding model or written to storage, so the raw value never reaches an external API or your durable record. Detected kinds: email, phone, SSN, credit-card number. Three strategies you pick per digest: redact , hash , shift_date . Honest scope: masking covers the text subject, text objects, and string property values. It does not cover numbers, booleans, blobs, or PII glued inside a longer token. Why masking runs before storage Text values are embedded into a vector for semantic search. If raw personal data reached the embedding model, it would travel to an external model API and the resulting vector would encode it in a recoverable form. Masking happens before that call — and before the value is persisted — so neither the durable record nor the embedding ever holds the original. What is detected Detection is heuristic and deterministic — there is no ML classifier. Each whitespace-delimited token is classified most-specific-first to avoid false positives: Kind Detection rule Email An @ with a local part before it and a dotted domain after it; no whitespace. SSN Exactly 9 digits with hyphens, e.g. 123-45-6789 . Checked before phone to avoid ambiguity. Credit card 13–19 digits (spaces/hyphens allowed) that pass the Luhn checksum. Phone 10–15 digits with at least one delimiter ( - , ( , or a leading + ). Mask strategies You choose the strategy with the mask field on a digest request: mask Output for user@example.com Use case redact (default) [REDACTED] Irreversible; retains no signal. hash / hash_sha256 sha256:<12 hex chars> Equal values hash to the same token, so you can dedup or join without storing the original. shift_date An ISO date shifted by a fixed number of days Preserve date arithmetic while hiding exact dates. Non-dates fall back to [REDACTED] . Any unrecognised value falls back to redact . What gets masked Masking covers three fields of each candidate fact: subject — the free-text subject string. object — only text-valued objects. Entity references, numbers, booleans, and blobs pass through unchanged, so graph edges and typed values survive. properties — string values in the JSON properties map (e.g. reason / why annotations agents attach). It runs on the single write path that all REST and MCP ingest flows share, so digest, the remember tool, and the connector pipeline all mask identically. The same masking applies whether you send a pre-extracted batch of triples or a block of content for extraction. Example curl -X POST https://acme.abagraph.com/api/digest \ -H "Authorization: Bearer $ABAGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "content": "Alice (alice@example.com, +1-555-867-5309) joined Acme on 2024-03-15.", "content_type": "text/plain", "source": "hr-system", "mask": "hash_sha256", "extract_model": {"provider": "anthropic", "model": "claude-opus-4-5", "key": "<your-key>"}, "judge_model": {"provider": "anthropic", "model": "claude-opus-4-5", "key": "<your-key>"} }' The pii_masked receipt Every write response that passes through masking includes a pii_masked count — how many tokens were replaced. 0 means nothing matched. Use it for monitoring and audit logging. { "written": 3, "written_ids": ["01J...", "01J...", "01J..."], "superseded": [], "candidates": 0, "entities": ["Person:alice", "Company:acme"], "pii_masked": 2 } What is NOT covered Be honest with yourself about the gaps — masking is defence-in-depth, not a guarantee: Detection works on whitespace-delimited tokens. PII glued inside a longer word with no surrounding spaces may slip through. Numeric and boolean objects are not scanned — they are typed and cannot carry an email/phone/SSN pattern. Blob objects are not scanned; handle their contents before ingestion. Detection is heuristic. If your data demands strict coverage, scrub upstream before you call digest. Define Your Own Policy — masking among your other policy knobs Tenant Isolation — masking runs per namespace, before storage BYOK Model Keys — keys used during digest are never persisted ### Tenant Isolation (security) Tenant Isolation TL;DR Use this page when you need to be certain one customer's data can never leak into another's results. Outcome first: every fact is stamped with a namespace at write time, and every read filters on it — so your key only ever returns your data, with no audit step required. The namespace is the only isolation chokepoint. Reads filter on it; conflict resolution is namespace-local. Filtering per query is the anti-pattern — isolation lives at the data-access layer, not in code you remember to add to each call. What a namespace is A namespace (the tenant) is a label attached to every fact when it is written. A namespace id is a non-empty string of up to 64 characters — ASCII letters, digits, and hyphens (e.g. acme , acme-prod-1 ). Your tenant API key is bound to exactly one namespace. The structural rule Every fact has a namespace. A query without a namespace filter would return ALL tenant data. The namespace is the only isolation chokepoint — reads filter on it, and conflict resolution is namespace-local. This is not a guideline, it is how the engine is built. There is a single point through which every read passes, and it applies the namespace filter there. Because the filter is applied uniformly after each query — never duplicated into individual endpoints — there is no path that returns facts without it. Writes stamp the namespace for you The namespace on a written fact is derived from your authenticated key, not from anything in the request body. A tenant caller cannot write into another namespace by supplying a different value — the field is overwritten with the key's namespace. Concrete example. Two customers each record where Sarah works: namespace=acme Person:sarah --works_at-- Org:acme valid_from=2024-03-01 recorded_at=2024-03-02 confidence=0.95 source=onboarding namespace=globex Person:sarah --works_at-- Org:globex valid_from=2024-03-01 recorded_at=2024-03-02 confidence=0.95 source=import Both stay Active . Neither customer can see the other's row. Reads filter on the namespace When you call a read endpoint, the result set is filtered to your namespace before it reaches you. You never add a tenant or namespace filter by hand — your key already determines it. An operator admin key is the only identity that can read across namespaces, and it is not issued to customers. Anti-pattern: filtering per query The dangerous design is to fetch broadly and then filter by tenant in application code on each query. The failure is structural: the one query where someone forgets the filter — a new endpoint, a debug script, a refactor — leaks every customer's data, and nothing fails loudly to catch it. Isolation therefore belongs at the data-access layer, applied once for all reads, not re-stated at every call site. That is exactly how the engine is built: a single read chokepoint applies the namespace filter, so a forgotten per-query filter cannot exist. When you build on top, keep the same discipline — rely on your key's namespace binding rather than reconstructing the filter yourself. Conflict resolution is namespace-local Supersession — a contradicting fact closing an older one — operates inside a single namespace. An assert in acme never closes a fact in globex , even when both share subject and predicate. Two customers can each hold an independent belief about the same entity without interfering. This is why the two Sarah rows above coexist. Namespaces over MCP The MCP surface uses the same model. Every tool call carries a namespace argument, but a tenant-scoped caller cannot override their pinned namespace through it — the argument is ignored and the call runs in the key's namespace. Each MCP call targets exactly one namespace, and the response is filtered to it. Unlike the REST surface, this filter applies even for an operator admin caller, because an MCP call is always single-namespace by construction. Branches and isolation A branch is a fork-and-reconcile overlay derived from your namespace. Branch facts are excluded from your normal reads; you see them only through an explicit branch read that reconciles the overlay against the base namespace. Promoting a branch merges it back; discarding it drops the overlay. A branch is never visible to another customer's namespace. See the Branches concept page. Namespace lifecycle (operator only) Creating, listing, and deleting namespaces is an operator-level action that requires an admin key. As a customer you receive a namespace and a key for it; you do not provision namespaces yourself. Related Define Your Own Policy — the namespace among your other policy knobs Authentication and Authorization — how your key's namespace is resolved PII Masking — masking runs per namespace, before storage Branches — fork-and-reconcile overlays