@locusgraph/client
v2.0.0
Published
TypeScript SDK for LocusGraph memory system
Maintainers
Readme
LocusGraph TypeScript SDK
TypeScript/JavaScript SDK for the LocusGraph memory system — store events, recall memories with semantic search, traverse relationships, and reason over them.
Documentation: docs.locusgraph.com/locusgraph — concepts, the HTTP API, and the guides. This page is the SDK reference.
npm install @locusgraph/clientUpgrading? 2.0 made every field you pass snake_case; 1.0 renamed four fields in the response.
Quick start
import { LocusGraphClient } from '@locusgraph/client';
const client = new LocusGraphClient({
server_url: 'https://api.locusgraph.com', // region server (see "Connecting")
agent_secret: process.env.LOCUSGRAPH_AGENT_SECRET,
graph_id: 'my-graph', // default graph for calls below
});
// Remember something
await client.storeEvent({
graph_id: 'my-graph',
event_kind: 'fact',
source: 'user',
context_id: 'preference:dark_mode',
payload: { data: 'prefers dark mode' },
});
// Recall it later
const result = await client.retrieveMemories({ query: 'what theme do they like?' });
console.log(result.memories);Concepts
A few ideas make the API click:
- Graph — an isolated memory space, addressed by an id. One per user, workspace, or project.
- Context — a named anchor in the graph, written
type:slug(e.g.preference:dark_mode,user:alice). It's the stable handle you address memories by. - Locus (memory) — an actual stored memory event tied to one or more contexts. You store and search loci; you address them by context. One context can have many loci (reinforcement); one locus can reference many contexts.
- Source (trust ladder) — every memory is stamped with where it came from. Higher = more trusted, and it feeds retrieval ranking. So a
verifiedfact outranks anassistantguess.
The slug is the filing location — keep it short and canonical so the same idea always lands on the same context.
Connecting
The SDK authenticates with an agent bearer secret and talks directly to a region server (e.g. https://api.locusgraph.com), not the dashboard/proxy host.
const client = new LocusGraphClient({
server_url: process.env.LOCUSGRAPH_SERVER_URL, // default: https://api.locusgraph.com
agent_secret: process.env.LOCUSGRAPH_AGENT_SECRET,
graph_id: process.env.LOCUSGRAPH_GRAPH_ID, // optional default graph
});| Option | Env var | Notes |
|--------|---------|-------|
| server_url | LOCUSGRAPH_SERVER_URL | Region server base URL |
| agent_secret | LOCUSGRAPH_AGENT_SECRET | Bearer token. Unscoped tokens can create graphs; graph-scoped tokens are limited to one graph |
| graph_id | — | Default graph; per-call graph_id overrides it |
graph_id can be passed per call; methods throw a clear error if no graph is resolvable.
API reference
All methods return the unwrapped response (the { success, data } envelope is handled for you) and throw an Error on non-2xx responses.
Graphs
// Create a graph (needs an unscoped secret)
const g = await client.createGraph('research-notes', 'Papers and findings');
// → { graph_id, name, description, owner_agent_id, created_at }
// List accessible graphs
const { graphs } = await client.listGraphs({ page: 0, page_size: 50 });Events (writing memory)
// One event
await client.storeEvent({
graph_id: 'my-graph',
event_kind: 'fact', // see "Event kinds"
source: 'user', // see "Sources"
context_id: 'person:alice', // primary context (type:slug)
related_to: ['org:acme'], // optional relationship links
extends: ['user:alice'],
payload: { data: 'Alice joined Acme' },
});
// Many events in one round-trip, processed IN ORDER (max 1000)
const batch = await client.storeEventsBatch([
{ event_kind: 'fact', context_id: 'service:auth', payload: { data: 'Auth API' } },
{ event_kind: 'fact', context_id: 'endpoint:login', extends: ['service:auth'], payload: { data: '/login' } },
], 'my-graph');
// → { stored, filtered, failed, results: [...] }Memories (reading)
// Semantic search
const result = await client.retrieveMemories({
query: 'what do we know about billing?',
limit: 10,
format: 'markdown', // 'markdown' (default) | 'toon' | 'json'
});
// Agentic deep recall — a walker agent explores the context graph (≤3 hops),
// retrieves with aimed queries, and returns a synthesized answer + evidence.
// Prefer over retrieveMemories for multi-hop questions.
const recall = await client.deepRecall({
question: 'What did we decide about churn, and why?',
context_ids: ['session:retention_research'], // optional starting hint
});
// → { summary, facts: [{ text, source, locus, contexts, match_score }],
// contexts_walked, coverage: 'full' | 'partial', rounds_used, fallback, usage }Advanced recall — retrieveMemories accepts:
| Option | What it does |
|--------|--------------|
| context_ids | Restrict search to these contexts (intersect) |
| boost_context_ids | Additively widen: pull in memories linked to these contexts even if text search misses them |
| coverageContextIds + coverageGroupPrefix | Spread a share of results across the contexts' timeline — for "list everything" questions |
| context_types | Filter by { type: [names] } |
| sources | Only these provenance sources (e.g. ['user', 'verified']) |
| expand_depth | Expand context hierarchy / locus links (default 1) |
// "Everything about Alice", even what text search would miss
await client.retrieveMemories({
query: 'recent activity',
boost_context_ids: ['user:alice'],
coverage_context_ids: ['user:alice'],
coverage_group_prefix: 'session:',
sources: ['user', 'verified'],
});Contexts (inspecting & curating)
await client.listContextTypes(); // types + counts
await client.listContextsByType('preference'); // all contexts of a type
await client.searchContexts('billing'); // keyword/name search
await client.getContextByName('dark_mode'); // by name (throws if 404)
await client.getContext({ context_id: 'preference:dark_mode' }); // by id
await client.batchGetContext(['preference:dark_mode', 'fact:works_at_acme']);
// Delete a context; cascades to its links and orphaned loci
await client.forgetContext({ context_type: 'preference', context_name: 'dark_mode' });Relationship-aware search. Pass include / format to fold in how matched contexts already connect:
const res = await client.searchContexts('dark', undefined, {
include: 'relationships',
format: 'mindmap', // 'json' (default) | 'mindmap' | 'toon'
});
console.log(res.relationships_text);
// preference:dark_mode
// └─ contradicted_by ← preference:light_modeformat: 'json'→res.relationships(a{ contextId: ContextLink[] }map)format: 'mindmap'/'toon'→res.relationships_text(one LLM-ready string)
Relationship traversal
// A context's edges
await client.getContextRelationships({
context_type: 'service', context_name: 'auth',
link_type: 'extends', // related_to | extends | reinforces | contradicts
direction: 'incoming', // outgoing | incoming | both
});
// Memories from related contexts, in one call
await client.getRelatedMemories({
context_type: 'service', context_name: 'auth',
link_type: 'extends', direction: 'incoming',
query: 'error rates',
});Resolution (forward references)
When a memory references a context whose locus doesn't exist yet, the link is left unresolved. Reconcile it later:
await client.getUnresolvedOverview(); // graph-wide unresolved links
await client.getUnresolvedLinks('person:alice'); // for one context
await client.resolve({ context_id: 'person:alice', locus_id: 'locus_123' });
await client.batchResolve({ resolutions: [{ context_id: '...', locus_id: '...' }] });Event kinds
event_kind categorizes a memory and maps to an internal kind + a default source.
| Group | Kinds |
|-------|-------|
| Knowledge | fact, knowledge, observation, learned, feedback |
| Rules | constraint, rule, constraint_violation |
| Actions | action, task, operation, execution, completed |
| Decisions | decision, choice, selection, determination |
| Routine | routine, heartbeat, status |
| Noise | noise, debug, log (filtered from ranking) |
Sources (trust ladder)
source is the provenance of a memory. Higher trust = higher base confidence, and confidence is ~25% of retrieval rank — so the source you stamp matters when memories conflict.
| Source | Trust | Meaning | Aliases |
|--------|------|---------|---------|
| policy | 0.95 | Organizational mandate | |
| verified | 0.90 | Authoritative / validated | validator |
| tool | 0.80 | Reliable tool output | executor |
| document | 0.75 | Published source (e.g. a file) | |
| user | 0.70 | User feedback | |
| assistant | 0.60 | Model inference | agent, model |
| derived | 0.55 | Second-hand synthesis | |
| system | 0.50 | System-generated | |
Context links
Connect contexts when storing an event (each takes context ids):
| Field | Meaning |
|-------|---------|
| context_id | The memory's primary context |
| extends | This is a more specific detail of another (hierarchy) |
| related_to | Lateral association |
| reinforces | Strengthens an existing memory |
| contradicts | Conflicts with / supersedes an existing memory |
Error handling
Methods throw an Error with the status and body on failure; single-context reads throw on 404.
try {
await client.getContext({ context_id: 'missing:thing' });
} catch (err) {
console.error(err.message); // "Context not found"
}TypeScript
Fully typed — every request and response has an exported interface (CreateEventApiRequest, ContextQuery, ContextSearchResponse, …). Import them from the package root:
import { LocusGraphClient, type ContextQuery, type StoreEventResponse } from '@locusgraph/client';Upgrading to 2.0
Every field you pass is snake_case, including the ones on the constructor.
Before, the constructor took graphId and the call beside it took graph_id,
and four option objects wanted both spellings at once.
// 1.x
const client = new LocusGraphClient({ serverUrl, agentSecret, graphId });
await client.getContext({ graphId, context_id: "fact:pricing" });
// 2.0
const client = new LocusGraphClient({ server_url, agent_secret, graph_id });
await client.getContext({ graph_id, context_id: "fact:pricing" });The rule is now one sentence: what you send and what you get back are spelled the same way, because both are the wire. Nothing is translated on the way out, so there is no second vocabulary to learn and nothing to keep in step.
Renamed: serverUrl, agentSecret, graphId, contextType, contextName,
linkType, contextIds, contextTypes, expandDepth, semanticWeight,
includeSelf, observedAt, fromContextId, toContextId,
boostContextIds, coverageContextIds, coverageGroupPrefix.
Positional arguments are unchanged: getLocus(locusId, graphId) still takes two
strings in that order.
New in 2.0: offset on a search, beside the has_more that 1.0 added.
Upgrading to 1.0
The engine gave each thing on the wire one name, and 1.0 follows it. Four fields
moved; nothing was added to replace them, so an 0.x client reads undefined
against the current API rather than failing loudly.
| 0.x | 1.0 | why |
|---|---|---|
| event_id | locus_id | it named the context in a request and the memory in a response, and two events written to one context returned the same value under a name promising per-event identity |
| kind | value | it sat beside event_kind meaning something unrelated: not what you wrote, but how long the memory survives decay. Values are lowercase now — high, medium, low, permanent |
| relevance | confidence | it was confidence bucketed into three, under a word that means the query match score everywhere else. The number is unbucketed now |
| score | match_score | indistinguishable from confidence beside it. One belongs to the search, the other to the memory |
status changed with them. A write used to answer recorded or filtered; it
now says what happened to the context:
created the context held nothing
unchanged byte-identical to what it held — a retry, not an edit
replaced different text, and the write sent replace: true
filtered the admission gate did not admit itWriting text a context does not already hold without replace: true is
refused with 409, and the refusal carries held — the memory it would have
written over, with its text, source and confidence — so you can decide in one
round trip.
New, and unread by 0.x: offset and has_more on a search, committed on a
transaction.
Documentation
docs.locusgraph.com/locusgraph — what a context is, how the admission gate decides what becomes a memory, and the HTTP API this client speaks.
License
MIT
