{"params":["tenantId","entityToken","query","queryEmbedding","limit"],"requiredQueries":["facts","interactions"],"queries":[{"name":"facts","required":true,"requiredColumns":["fact"],"optionalColumns":["weight","createdAt","topics"],"enables":"Retrieval, narrowing, and voice — the floor.","description":"The substance: what's known about this entity. Should pull WIDE (~200 rows) — its job is a good candidate pool, not a precise answer; the brain narrows. ORDER MATTERS: the first ~5 rows are shown to the voice unconditionally, never gated by narrowing — rank your most foundational/identity facts (name, role, who this person is) first, not just by recency or weight. Supply `createdAt` to also enable the Temporal engine."},{"name":"interactions","required":true,"requiredColumns":["userMessage"],"optionalColumns":["assistantMessage","createdAt"],"enables":"Conversational continuity; input to the Beauty engine.","description":"Recent turns between the user and the app, most recent first."},{"name":"identity","required":false,"requiredColumns":["relation","name"],"optionalColumns":["label","description"],"enables":"The always-on identity block — who this entity actually is.","description":"One row per thing this entity is related to: what they've built, believe, are interested in, where they're based. `relation` is your own vocabulary (CREATED, BELIEVES, INTERESTED_IN…) — the brain groups rows BY relation and renders them compactly, so return one row per item and let the brain do the grouping. Supply `description` wherever you have one — 'Believes: consistency' is a tag and reasons about nothing; 'Believes: consistency — that a companion who forgets is worse than no companion' is a proposition the voice can actually use. This is shown to the voice unconditionally and never competes with the `facts` budget, which is the whole point: identity should not have to win a ranking contest against whatever was said most recently."},{"name":"connections","required":false,"requiredColumns":["from","rel","to"],"optionalColumns":["strength","fromLabel","toLabel"],"enables":"The associative layer — how the things in THIS message relate to each other.","description":"Relationships worth knowing for this turn. Use `$query` to find the entities named in the user's message and return their edges (one hop is usually right), ordered by strength. Falling back to the entity's strongest structural edges when the message names nothing is a good default. Without this the voice sees facts as a flat list with no idea which of them are connected — it can hold two related things at once and never link them."},{"name":"directives","required":false,"requiredColumns":["instruction"],"optionalColumns":["weight","origin"],"enables":"Standing instructions — how this entity has asked to be treated.","description":"Durable rules, not facts: 'never quote my own message back at me', 'no emojis', 'don't put download buttons on artifacts'. Shown to the voice unconditionally and never ranked against facts, because a correction someone had to give twice must not have to win a relevance contest to be honoured. Order by weight if you have one. This is also the natural home for corrections the app captures from conversation — the brain reads them, your app decides what becomes one."},{"name":"emotionalState","required":false,"requiredColumns":["mood"],"optionalColumns":["intensity","readAt","source"],"enables":"Beauty engine — emotional read of the moment.","description":"The most recent mood signal, if the app tracks one. One row."},{"name":"location","required":false,"requiredColumns":[],"optionalColumns":["locationName","isMoving","timezone","atDestination","latitude","longitude","heartRate","stepsLast30m","sleepQualityLastNight","capturedAt"],"enables":"Situational grounding for Beauty and voice.","description":"Where the entity is right now, if the app knows. One row."},{"name":"calibration","required":false,"requiredColumns":[],"optionalColumns":["stated","baseline","silentSnapshots"],"enables":"Subjective calibration and receptivity — what a word means when THIS person says it.","description":"Stated sensations paired with the conditions they were felt in, plus the conditions lived through without comment. One row."},{"name":"openThreads","required":false,"requiredColumns":["summary"],"optionalColumns":["kind","openedAt"],"enables":"Curiosity engine — unfinished business worth returning to.","description":"Intents, unanswered questions, things left hanging."},{"name":"topTopics","required":false,"requiredColumns":["name"],"optionalColumns":["count","lastSeen"],"enables":"Curiosity and Temporal engines — what this entity keeps returning to.","description":"Most-discussed topics, most frequent first."},{"name":"reminders","required":false,"requiredColumns":["message"],"optionalColumns":["id","deliverAt"],"enables":"Curiosity engine — things due or promised.","description":"Pending reminders or commitments."}],"roles":{"available":["voiceChat","relevanceEditorial","beautyEmotional","temporalNarrator","curiosityNarrator","ingestDecompose"],"defaults":{"voiceChat":{"provider":"anthropic","model":"claude-sonnet-5"},"relevanceEditorial":{"provider":"anthropic","model":"claude-haiku-4-5-20251001"},"beautyEmotional":{"provider":"anthropic","model":"claude-haiku-4-5-20251001"},"temporalNarrator":{"provider":"anthropic","model":"claude-haiku-4-5-20251001"},"curiosityNarrator":{"provider":"anthropic","model":"claude-haiku-4-5-20251001"},"ingestDecompose":{"provider":"anthropic","model":"claude-haiku-4-5-20251001"}}},"endpoint":{"path":"/api/v1/filter","method":"POST","body":{"entityToken":"string, required — whose context this is","message":"string, required — the user's turn","history":"ChatMsg[], optional — prior turns, oldest first; the brain doesn't store conversation state","policy":"string, optional — overrides the default narrowing policy","task":"string, optional — overrides what the voice is told to do","timezone":"string, optional but STRONGLY recommended — the person's IANA zone, e.g. 'America/New_York'. Your client already knows it (Intl.DateTimeFormat().resolvedOptions().timeZone in a browser, the device on mobile), and it follows them when they travel, so per-call beats anything stored. Without it the brain reasons in UTC and will deliberately refuse to discuss their local time or their day rather than tell someone it's 5am when it's 1am for them. Hour-of-day rhythm detection is also computed in this zone.","attachments":"Attachment[], optional — { type: 'image'|'document', mediaType, data (base64) }. Both go to the voice directly (Claude reads images and PDFs natively). Video is deliberately not supported — no precedent as brain infrastructure; pre-process it into text in your own app before calling this.","allowArtifacts":"boolean, optional, default true — whether the voice may return self-contained artifacts (games, docs, code panels)","personaName":"string, optional — overrides the companion's NAME for this turn only. The registered persona is per-tenant, but a name can be per-person: one app may be one thing to the person who built it and something else to everyone else. Only the name changes; the tenant's registered voice directives still apply. Note it sits in the cached prompt prefix, so each distinct name is its own cache entry.","webSearch":"boolean, optional, default false — live web access for this turn (Anthropic's server-side search, Google Search grounding on Gemini). Deliberately per-call: it costs money twice over, a per-search fee and results landing in the prompt as input tokens, so set it only on turns that genuinely need current information.","maxWebSearches":"number, optional, default 3 — cap on searches within one turn when webSearch is set","decompose":"boolean, optional, default false — break this message into atomic facts, returned as `proposedAtoms`. Retrieval narrows to atomic facts before generation, so how atomic your WRITES were sets the ceiling on how well narrowing can work; this is the brain doing that decomposition for you instead of every app reinventing it. Per-call and off by default because it spends a model call (the `ingestDecompose` role) — but it runs beside the engines and nothing downstream waits on it, so the cost is money, not latency.","roles":"{ [roleName]: {provider, model, fallbackProvider?, fallbackModel?} }, optional — model choice for THIS turn, winning over the roles you registered. Registered roles are per-tenant, but the right model is often per-ENTITY: a cheap triage tier and an escalated one, a bit player and a principal. Which one is being served is only known per call. Every role is overridable, not just the voice, but only the ones you name — anything unnamed still resolves from your registered roles, then the defaults. Every role but voiceChat is an engine/picker/ingest call, so naming only voiceChat moves the expensive call but not the call count. Same role names and shape as registration; an unknown role name or malformed choice is a 400 here.","stream":"boolean, optional — SSE mode: `delta` events as the voice speaks, one terminal `done` event with the full response. The `done` payload is authoritative; deltas are a preview and may contain raw artifact tags mid-stream."},"response":{"message":"string — the voice's reply, artifact tags already stripped","silence":"boolean","factsUsed":"string[] — verbatim facts the voice was given, auditable against your graph","observations":"EngineCandidate[] — what the engines noticed; persist it if you want","artifacts":"Artifact[] — { type: 'text/html'|'code', title, language?, content }","proposedDirectives":"string[] — standing instructions the entity appears to have just stated. PROPOSALS; the brain writes nothing. Each is verified to quote the actual message, so a rule the model inferred rather than read is discarded before you see it. Whatever you persist should come back through your `directives` query next turn.","proposedAtoms":"{ atom, topics?, supersedes? }[] — this message broken into atomic facts. Empty unless you set `decompose`. PROPOSALS on the same terms: each atom is verified to quote the message, and a `supersedes` names a fact that was genuinely in your graph this turn, verbatim, so you can retire it rather than store both. Shaped to match the `facts` contract — `atom` is a `fact`, `topics` is `topics` — so writing them back is close to a direct mapping.","meta":"token counts, which engine's candidate was picked (if any), degraded queries, etc."}},"ingest":{"path":"/api/v1/ingest","method":"POST","availability":"Brain-managed graphs only — see the notes below. A connection registered with your own schema returns 409, by design.","body":{"entityToken":"string, required — whose graph this is written into","facts":"{ text, weight?, topics?, supersedes? }[], optional, max 200 per request. Shaped to take `proposedAtoms` from a /filter response almost directly: `atom` becomes `text`, `topics` and `supersedes` carry across. Deduped on a content hash (whitespace and case folded), so retrying a request updates rather than duplicating. `supersedes` names a fact to retire — it is stamped with a date, never deleted, so 'you used to...' survives.","person":"{ role?, basedIn?, background?, communicationStyle? }, optional — an allowlist, not a pass-through. Other keys are dropped rather than written.","interaction":"{ userMessage, assistantMessage?, id? }, optional — the turn itself, so the `interactions` query has something to read next time. NOT deduped on content (someone can say 'ok' twice); supply your own `id` if you need a retry to be idempotent."},"response":{"ok":"true","facts":"number written","superseded":"number retired","interactions":"number written"},"notes":["Not metered against your turn quota. A blocked turn costs a reply you can retry; a blocked write loses your user's data.","One transaction per request — a partial ingest would leave the graph asserting a conversation it knows nothing about."]},"network":{"enrolment":"Opt-in per tenant. Register with `networkEnrolled: true`; off otherwise.","idea":"Discovery by NEED, not by address. Nobody can look anyone up: a person tells your app what they're looking for, your app runs a cross-instance query it declared in advance, and offers come back by opaque handle. Because there is no way to name a person, there is no way to contact one uninvited — that property is structural, not a policy.","endpoints":{"POST /api/v1/network/offers":"Publish what someone can do. Requires `consent: { statement, at }` — a recorded agreement from the person, because publishing them into an index every enrolled tenant can search is the moment consent matters. Nothing identifying is accepted or stored; they are represented by a handle the brain mints.","DELETE /api/v1/network/offers":"Withdraw one offer, or omit `key` to withdraw all of theirs.","POST /api/v1/network/search":"Run one of your registered cross-instance queries. Explicit by design — no engine calls this and nothing infers it, so the network costs nothing on turns that aren't looking for anything.","GET /api/v1/network/introductions":"`box=inbox` (default) for requests waiting on your user, `box=outbox` for ones they made. Write inbox items into your own graph and your existing `openThreads` / `reminders` queries will surface them, so your companion raises it in conversation.","POST /api/v1/network/introductions":"Ask for an introduction to a handle. Discloses nothing.","POST /api/v1/network/introductions/respond":"Your user's answer, and the only path by which identity moves. `release` is what THEY chose to share — the brain never assembles it from what it knows about them. Declining is final: asking again returns the same declined record."},"queries":"Cross-instance search is served by the brain's own findOffers query — no Cypher to register — and validated four ways: read-only, no $tenantId/$entityToken (a network query that could scope by them could correlate a person across apps), it runs, and its columns are a STRICT SUBSET of what the contract permits. That last one differs from your graph queries, where extra columns are harmless — here an extra column is how identity would escape, so it fails registration.","params":["query","queryEmbedding","limit"],"returnableColumns":["handle","capability","locale","summary","tags","publishedAt","score"]},"notes":["Queries are read-only. Write clauses are rejected at registration, and every session runs in READ access mode. That is unconditional — it applies to managed graphs too.","Missing optional queries are not errors — the engine that needed them simply contributes nothing.","The `facts` query should pull wide (~200 rows). The brain narrows; the query does not have to.","Bring your own LLM key: you pay your provider directly.","The brain never writes to YOUR schema. On a graph you registered, it proposes (facts, observations) and you persist — your app's own ingestion is what keeps facts/interactions fresh for the next turn. The brain owns no schema of yours and could not MERGE into a shape it has never seen.","The exception is a brain-MANAGED graph: one the brain provisioned, registered with `managedSchema: true`, carrying the brain's own schema (:Person/:Fact/:Interaction, scoped by tenantId + entityToken). There, POST /api/v1/ingest writes for you. It is off unless you opted in, and it is not offered for a graph running your own schema — ingest would report success while filling it with nodes your queries never read."]}