glove-memory is the memory layer for Glove. Storage-agnostic adapter contracts, schema-first ontology, and auto-registered tool surfaces. Four complementary, independently usable subsystems with bring-your-own storage.
Entity, episodic, and resources use a reader / curator split — readers attach to the conversational agent, curators run as orchestrator-driven extractors. Context is different: it's user-configured rather than curator-extracted, so it uses a single registration that gives the agent both read and write tools plus system-prompt injection.
Entity memory. Graph-shaped, schema-first, deterministic identity resolution. Nodes have a class (Person, Organization, …), a Zod-validated property bag, and identity keys that the curator uses to upsert without duplicates. Relationships connect nodes by typed edges. The query DSL supports traversal, predicates, and bounded fan-out.
Episodic memory. Timeline-bound, append-only, semantically searchable. An Episode has a registered kind (e.g. meeting), a list of participant entity ids, an occurrence time, free-form properties, and a content field that gets embedded out-of-band for semantic search.
Resources. A POSIX-style virtual filesystem the agent navigates with ls / read / grep / glob / edit. Roots are declared in the schema so the agent only ever sees configured trees. Files carry metadata including links that point at entity ids, episodes, or other paths — the same reverse-lookup primitive the reconciliation primitives consume.
Context. User-configured ambient context, auto-injected into the system prompt every turn. Small surface (4 tools), the agent both reads and writes, and a wrapper composes pinned entries after the developer's system prompt before each model call.
The package ships a top-level barrel plus subpath exports that keep consumer dependencies tight.
| Import | Contents |
|---|---|
glove-memory | Barrel |
glove-memory/core | Shared types — Provenance, Link, EmbeddingAdapter, MemorySchema, errors |
glove-memory/entity | EntityMemoryAdapter contract, query DSL, types |
glove-memory/episodic | EpisodicMemoryAdapter contract, Episode types, semantic-search opts |
glove-memory/resources | ResourceFsAdapter contract, file types, POSIX path helpers |
glove-memory/context | ContextAdapter contract, ContextEntry type, default markdown rendering |
glove-memory/tools | Auto-registered read/write tool factories and useMemory* / useEpisodic* / useResources* / useContext helpers |
glove-memory/in-memory | Reference in-process adapters for dev/test |
Every memory deployment starts with a MemorySchema. It declares node classes (with identity keys for deterministic upsert), relationship types, episode kinds, and resource roots. Tool descriptions render only the slice of the schema each role needs, so the schema is also what bounds prompt surface.
import { MemorySchema } from "glove-memory";
import { z } from "zod";
const schema = new MemorySchema()
.defineNodeClass({
name: "Person",
schema: z.object({ name: z.string(), email: z.string().optional() }),
identityKeys: [["email"], ["name"]],
searchableProperties: ["name", "email"],
})
.defineNodeClass({
name: "Organization",
schema: z.object({ name: z.string(), domain: z.string().optional() }),
identityKeys: [["domain"], ["name"]],
searchableProperties: ["name"],
})
.defineRelationship({ type: "worksAt", from: "Person", to: "Organization" })
.defineEpisodeKind({ name: "meeting", description: "A scheduled gathering." })
.defineResourceRoot({ path: "/research", description: "External research artifacts." })
.defineResourceRoot({ path: "/transcripts", description: "Meeting transcripts." });If you're building an agent that needs memory access, we advise against attaching the entity / episodic / resources tools directly to your main Glove instance. Build subagents — one per retrieval task — and register them on the main agent. Each subagent attaches only the adapter slice it needs; the main agent stays small and routes to the right subagent based on what the user asked for.
Why:
lookup" is a tighter signal than "you have these eight memory tools, decide which to call."useMemoryReader cannot write — the affordance isn't there. The main agent never has to be told "don't accidentally create entities mid-conversation"; it structurally can't.The exception is useContext. Context is small (4 tools), user-driven ("remember that…"), and ships with the system-prompt-injection wrapper that has to live on the agent the user actually talks to. Keep useContext on the main agent.
import { Glove } from "glove-core";
import {
InMemoryEntityAdapter,
InMemoryEpisodicAdapter,
InMemoryResourcesAdapter,
InMemoryContextAdapter,
useMemoryReader,
useEpisodicReader,
useResourcesReader,
useContext,
} from "glove-memory";
const entity = new InMemoryEntityAdapter({ schema });
const episodic = new InMemoryEpisodicAdapter({ schema, embedder });
const resources = new InMemoryResourcesAdapter({ schema, embedder });
const context = new InMemoryContextAdapter({ schema });
// `lookup` — answers "who is Don?", "what do you know about Acme?". Sees
// only the entity graph; doesn't render episode kinds or resource roots.
const lookupFactory = ({ parentStore, parentControls }) =>
useMemoryReader(
new Glove({
store: parentStore,
model,
displayManager: parentControls.displayManager,
systemPrompt:
"You answer factual questions about people, organizations, and their " +
"relationships. Use glove_memory_find for fuzzy lookups, glove_memory_get " +
"for one-hop neighbourhoods, glove_memory_query for deeper traversal.",
compaction_config: { compaction_instructions: "..." },
serverMode: true,
}),
entity,
);
// `recall` — answers "what did we discuss with Don last week?". Reads
// episodes; reads entity for resolving names to ids.
const recallFactory = ({ parentStore, parentControls }) => {
let glove = new Glove({
store: parentStore,
model,
displayManager: parentControls.displayManager,
systemPrompt:
"You answer questions about past events. Resolve participant names to " +
"ids via glove_memory_find first, then use glove_episodic_timeline / " +
"glove_episodic_find / glove_episodic_search depending on whether the " +
"user asked about a specific person, a window, or a topic.",
compaction_config: { compaction_instructions: "..." },
serverMode: true,
});
glove = useMemoryReader(glove, entity);
glove = useEpisodicReader(glove, episodic);
return glove;
};
// `find-notes` — answers "what notes do we have on Aptos regulation?".
// Browses the filesystem; reads entity for "notes about <person>".
const findNotesFactory = ({ parentStore, parentControls }) => {
let glove = new Glove({
store: parentStore,
model,
displayManager: parentControls.displayManager,
systemPrompt:
"You find research notes, transcripts, and link collections in the " +
"resource filesystem. Use glove_resources_grep / _glob / _search to " +
"locate files; glove_resources_read to fetch their contents. When the " +
"user asks for notes about a specific person or organization, look up " +
"the entity id first and use glove_resources_links_for to find " +
"everything that links to it.",
compaction_config: { compaction_instructions: "..." },
serverMode: true,
});
glove = useMemoryReader(glove, entity);
glove = useResourcesReader(glove, resources);
return glove;
};
// Main agent — keeps useContext for the system-prompt injection and the
// small "remember that..." tool surface, but offloads every other memory
// task to a subagent.
const main = useContext(new Glove({ /* ... */ }), context)
.defineSubAgent({ name: "lookup", description: "Look up people, organizations, and their relationships.", factory: lookupFactory })
.defineSubAgent({ name: "recall", description: "Recall past meetings, decisions, and events.", factory: recallFactory })
.defineSubAgent({ name: "find-notes", description: "Find research notes, transcripts, and links.", factory: findNotesFactory })
.build();The shape generalises: any subagent the developer registers picks the smallest combination of use*Reader / use*Curator calls that makes its job possible. Reader-only when it's just resolving ids or summaries; curator when it actually needs to mutate; nothing at all when memory isn't relevant.
The same advice applies to the curator. A parent curator that routes to specialised write-side subagents — entity-linker, episode-recorder, resource-writer — is preferable to a single curator with every write tool attached. Each subagent attaches only the adapters it needs, so its tool descriptions render only the schema slice for its role. The entity-linker never sees episode kinds; the episode-recorder gets a read-only view of entity classes (so it can resolve participant ids) plus the episode-kind list for writes; the resource-writer gets read access to entities and episodes so it can populate metadata.links correctly.
Subagents share the parent's adapters — there's no per-subagent memory namespace. What the linker writes, the recorder immediately reads.
import { Glove } from "glove-core";
import {
useMemoryCurator,
useMemoryReader,
useEpisodicCurator,
useEpisodicReader,
useResourcesCurator,
} from "glove-memory";
// Sees: node classes, relationships. NOT episode kinds, NOT resource roots.
const linkerFactory = ({ parentStore, parentControls }) =>
useMemoryCurator(
new Glove({ /* ... */ }),
entity,
);
// Sees: episode kinds (for writes) + read-only entity classes (to resolve
// participant ids). Does NOT see resource roots.
const recorderFactory = ({ parentStore, parentControls }) => {
let glove = new Glove({ /* ... */ });
glove = useMemoryReader(glove, entity);
glove = useEpisodicCurator(glove, episodic);
return glove;
};
// Sees: resource roots + read-only entities and episodes (so metadata.links
// points at real ids). Does NOT see write tools for entity / episodic.
const filerFactory = ({ parentStore, parentControls }) => {
let glove = new Glove({ /* ... */ });
glove = useMemoryReader(glove, entity);
glove = useEpisodicReader(glove, episodic);
glove = useResourcesCurator(glove, resources);
return glove;
};
// The parent curator owns no memory tools itself — it just routes. Its job
// is reading the conversation slice and dispatching to the right subagent
// in sequence (linker -> recorder -> filer).
const curator = new Glove({ /* ... */ })
.defineSubAgent({ name: "linker", description: "Extract entities and relationships.", factory: linkerFactory })
.defineSubAgent({ name: "recorder", description: "Record episodes; resolves participant ids first.", factory: recorderFactory })
.defineSubAgent({ name: "filer", description: "File research artifacts; resolves link targets first.", factory: filerFactory })
.build();Each subsystem auto-registers a focused set of tools. Read-on-demand tools attach via use*Reader; write tools attach via use*Curator. Context is the exception — it has a single registration that attaches read and write tools and the system-prompt-injection wrapper.
| Tool | Purpose |
|---|---|
glove_memory_find | Find nodes by class + filter, optional fuzzy |
glove_memory_get | Fetch a node by id + one-hop neighbourhood |
glove_memory_query | Full structured query via the query DSL |
glove_memory_add_node | Create or upsert a node by identity keys (curator) |
glove_memory_update_node | Patch a node's properties (curator) |
glove_memory_connect | Create or update an edge (curator) |
glove_memory_disconnect | Remove an edge (curator) |
glove_memory_merge_nodes | Fold one node into another (curator) |
| Tool | Purpose |
|---|---|
glove_episodic_search | Semantic search over episode content (only registered when adapter advertises supportsSemanticSearch) |
glove_episodic_find | Structured filter — by kind, participant, time range, properties |
glove_episodic_timeline | Chronological listing for an entity or time window |
glove_episodic_record | Append a new episode (curator) |
glove_episodic_update | Patch an existing episode (curator) |
glove_episodic_delete | Remove an episode (curator) |
| Tool | Purpose |
|---|---|
glove_resources_ls | List directory contents |
glove_resources_read | Read a file body, with optional line range |
glove_resources_stat | Get metadata about a single path |
glove_resources_grep | Text/regex search across the tree |
glove_resources_glob | Find paths by name pattern |
glove_resources_search | Semantic search (only registered when adapter advertises supportsSemanticSearch) |
glove_resources_links_for | Reverse-lookup: find resources linking to a target |
glove_resources_write | Create or overwrite a file (curator) |
glove_resources_edit | Replace a unique substring (curator) |
glove_resources_mkdir | Create an empty directory (curator) |
glove_resources_move | Rename or relocate (curator) |
glove_resources_remove | Delete a file or directory (curator) |
glove_resources_set_metadata | Patch metadata without rewriting body (curator) |
| Tool | Purpose |
|---|---|
glove_context_get | Read entries by section or list all |
glove_context_set | Add a new entry |
glove_context_update | Patch an existing entry in place |
glove_context_unset | Remove an entry or wipe an entire section |
useContext wraps Glove.processRequest. On every turn it calls adapter.render() to materialise pinned entries as a markdown block, composes <base systemPrompt> + \n\n + <rendered context>, and calls setSystemPrompt. Pinned context goes after the developer's system prompt — developer prompt sets agent character and guardrails; user context modifies engagement for this specific user. Re-rendering happens every turn, so external updates the user made between turns are reflected immediately.
Episodic and resources adapters generate embeddings out-of-band. Writes mark records embeddingStatus: "missing" (initial) or "stale" (content change) and return immediately. A separate process — typically a Station signal — picks them up via findEpisodesNeedingEmbedding / findFilesNeedingEmbedding, calls the configured EmbeddingAdapter, and writes vectors back via setEmbedding.
The EmbeddingAdapter contract is intentionally tiny — consumers plug in whatever provider they want without the package taking on a model dependency.
updateEpisode flips embeddingStatus: "stale" and drops the cached vector only when the content field changes — kind / participant / property / occurredAt patches don't re-embed. The embedding represents content; the spec is silent on the others. Consumers wanting different behaviour can delete + re-record.searchEpisodes ranks by (1 - recencyWeight) * semanticScore + recencyWeight * recencyScore where recencyScore = exp(-ln(2) * ageMs / halfLifeMs), halfLifeMs = 30 days. Default recencyWeight = 0.2. Companion adapters (sqlite/postgres) may pick different curves; only the shape of the blend is fixed by the spec.The package's contract is deliberately narrow: store, query, write, search. It does not cascade across adapters. When an entity is merged or deleted, episodes that reference its old id don't update on their own. Orchestrators reach for the cross-adapter primitives instead — most importantly episodic.replaceParticipantId and resources.replaceLinkTarget for the merge case.
| Action | Primitive |
|---|---|
| Entity merged | episodic.replaceParticipantId(oldId, newId, prov), resources.replaceLinkTarget("entity", oldId, newId, prov) |
| Entity deleted | episodic.findEpisodes({ where: { participantIds: [id] } }), resources.linksFor("entity", id) then orchestrator decides |
| Resource moved | resources.replaceLinkTarget("resource", fromPath, toPath, prov) |
| Episode deleted | resources.linksFor("episode", id) then orchestrator decides |
| Stale embeddings | findEpisodesNeedingEmbedding / findFilesNeedingEmbedding → embed → setEmbedding |
The package ships in-process reference adapters under glove-memory/in-memory: InMemoryEntityAdapter, InMemoryEpisodicAdapter, InMemoryResourcesAdapter, and InMemoryContextAdapter. They're intended for development and tests — every adapter contract is implemented end to end so you can wire up a full schema, exercise the tool surfaces, and write integration tests without standing up a database.
Companion storage backends ship as separate packages — glove-memory-sqlite and glove-memory-postgres — and are not part of the v0.1 release.
EmbeddingAdapter.set / update / unset; the UI / API / form / wherever users edit their preferences calls those directly.. and .. path resolution. All paths are absolute.