@kindgi/memory
npm install @kindgi/memory · source
Type contract for Kindgi memory: typed, versioned facts, the hash-chained run log, retrieval hits, and MemoryQueryBinding, the interface for querying facts and appending to the run log. The package holds no state and ships no storage; its only runtime export is the LOG_KINDS constant. A runtime adapter implements the binding (the Kindgi runtime provides a Postgres-backed one), and any object that satisfies the interface can stand in for it.
Purpose
Section titled “Purpose”Give agent code, tools, and storage backends one shared vocabulary for memory, so that code that reads facts (for example the retrieval step in @kindgi/agents) depends only on an interface and never on a database client. Every operation is tenant-scoped: callers pass tenantId on each call, and the binding implementation enforces isolation.
Exports
Section titled “Exports”- Facts
Fact<TContent>— a typed, versioned record:id,type,scope,version(monotonic within(scope, id)),createdAt, and optionalupdatedAt,content,contentRef(blob://<provider>/<bucket>/<key>for payloads stored externally),contentHash,size,embeddingModel,retention,source,causedByLogId,supersedes. A fact withoutsourceis immutable output ("Kind A"); a fact withsourceis a cached view of external state ("Kind B").MemoryScope— where a fact or log entry lives:tenantIdplus optionaluserId,orgId,projectId,threadId,sessionId.Retention— fact-level override of the tenant's retention defaults:keepUntil,keepDays,legalHold.Source,SourceFreshness,SourceRefresh— external-source metadata for Kind B facts: the sourcekind(http-api,blob,mcp-tool,external-db,user-input) anduri, freshness data (ttlSeconds,lastVerifiedAt,etag,sourceVersion), and the refreshstrategy(on-read,background,manual) with an optional registeredhandlerid.
- Log
LogEntry— one immutable entry in a run's append-only log, ordered bysequencewithin(tenantId, runId). Each entry storesprevHashandentryHash, so changing an earlier entry breaks every later link in the chain.LOG_KINDS/LogKind— the closed set of entry kinds (user-message,agent-message,system-message,tool-call,tool-result,internal-thought,retrieval,artifact-produced,event-emitted,event-received,guardrail-triggered,wait-suspended,wait-resumed), matching the enum inmemory.schema.jsonfrom@kindgi/specs.
RetrievalHit<TContent>— afactplus a numericscore. Keyword scores are unbounded positive ranks; semantic scores are cosine similarity in [-1, 1]. Higher is better in both cases.MemoryQueryBinding— the data-access interface, implemented by a runtime adapter. Every method returnsPromise<Result<…, MemoryError>>.listFacts(input: ListFactsInput)— facts filtered bytypeand a partialscope, capped bylimit;latestOnlyselects the latest version of each fact.searchByKeyword(input: SearchByKeywordInput)— full-text search over facts, returning up totopKhits.searchBySemantic(input: SearchBySemanticInput)— embedsquerythrough the caller-suppliedembeddingRegistryfrom@kindgi/embedding(pinned toembeddingModel, or the registry's only provider when omitted) and runs a vector search.appendLog(input: AppendLogInput)— appends one entry. The implementation assignssequenceand the hash chain and returns the storedLogEntry.readLog(input: ReadLogInput)— the entries for(tenantId, runId)in order, optionally narrowed withsinceSequence.
- Errors —
MemoryError, a union ofInvalidLogEntryError,InvalidFactError,FactNotFoundError,LogNotFoundError,RefreshHandlerMissingError,RetentionViolationError(reason:legal-hold,keep-untilorkeep-days),PersistenceError, and theEmbeddingErrorvariants from@kindgi/embedding. Every variant carries acodefor matching.
Example
Section titled “Example”import type { Fact, MemoryQueryBinding } from '@kindgi/memory';import type { ProjectId, RunId, TenantId } from '@kindgi/types';
interface Clause { readonly heading: string; readonly text: string;}
/** True when a cached-view (Kind B) fact is older than its source's TTL. */function isStale(fact: Fact, now = Date.now()): boolean { const freshness = fact.source?.freshness; if (freshness?.ttlSeconds === undefined || freshness.lastVerifiedAt === undefined) return false; return now - Date.parse(freshness.lastVerifiedAt) > freshness.ttlSeconds * 1000;}
export async function findClauses( memory: MemoryQueryBinding, tenantId: TenantId, projectId: ProjectId, runId: RunId, query: string,): Promise<readonly Clause[]> { const hits = await memory.searchByKeyword<Clause>({ tenantId, query, type: 'acme.clause', scope: { projectId }, topK: 5, }); if (hits.kind === 'err') { console.error(`clause search failed: ${hits.error.code}`, hits.error.message); return []; }
// Record the lookup in the run's hash-chained log. const logged = await memory.appendLog({ tenantId, runId, kind: 'retrieval', scope: { tenantId, projectId }, actor: 'acme.clause-finder', payload: { query, factIds: hits.value.map((hit) => hit.fact.id) }, }); if (logged.kind === 'err') console.error('log append failed', logged.error.message);
return hits.value .filter((hit) => !isStale(hit.fact)) .flatMap((hit) => (hit.fact.content === undefined ? [] : [hit.fact.content]));}Non-goals
Section titled “Non-goals”- No implementation. Storage, full-text and vector indexes, embedding calls, refresh handlers, and retention sweeps belong to the binding implementation. This package defines shapes only.
- No provenance on the interface. An implementation may record provenance internally, but
MemoryQueryBindingnever accepts or returns a provenance builder. - No per-fact retrieval hints. Which indexes a fact type populates is configured once per type in the memory implementation, not on individual facts.
- No pagination cursor.
listFactstakes alimitonly.
Related
Section titled “Related”@kindgi/agents— takes aMemoryQueryBindingand uses it for the retrieval step of each agent turn.@kindgi/embedding— the embedding provider registry used bysearchBySemantic.@kindgi/specs—memory.schema.json, the wire schema for facts and log entries.@kindgi/types— branded ids (FactId,LogEntryId,RunId, …) andResult.