@drover/memory
MemoryAdapter, in-memory and markdown impls, remember/recall/forget tools.
Implements the memory module — self-curated knowledge across runs.
MemoryEntry
interface MemoryEntry {
id: string; // ULID
scope: "global" | "agent" | "run";
agentId?: string; // present when scope ∈ {agent, run}
runId?: string; // present when scope === "run"
kind: "user" | "feedback" | "project" | "reference";
summary: string; // ≤200 chars
body: string; // markdown; recommended ≤2000 chars
tags?: readonly string[];
createdAt: number;
updatedAt?: number;
}MemoryAdapter
interface MemoryAdapter {
readonly id: string;
put(draft: MemoryDraft): Effect<MemoryEntry, MemoryError>;
get(id: string): Effect<MemoryEntry | null, MemoryError>;
search(opts: SearchOpts): Effect<readonly MemoryHit[], MemoryError>;
list(filter: Omit<SearchOpts, "query">): Effect<readonly MemoryEntry[], MemoryError>;
forget(id: string): Effect<boolean, MemoryError>;
close(): Effect<void, MemoryError>;
}put is upsert by id; omit to mint a fresh ULID. search ranks via
BM25 when query is set, by recency otherwise. forget returns
false if the id wasn’t present.
SearchOpts
interface SearchOpts {
query?: string;
scopes?: readonly MemoryScope[]; // default ["global", "agent"]; +"run" when runId set
agentId?: string;
runId?: string; // required when "run" ∈ scopes
kinds?: readonly MemoryKind[];
tags?: readonly string[]; // matches on ANY overlap
limit?: number; // default 20
}createMarkdownMemory
function createMarkdownMemory(opts: { root: string }): Promise<MemoryAdapter>;Layout:
<root>/
├── global/<id>.md
├── agents/<agentId>/<id>.md
└── runs/<runId>/<id>.md
YAML frontmatter + markdown body. Builds an in-process index on construction. Writes go through a Promise-chain mutex (single writer per process). Cross-process modifications aren’t seen until the next boot.
createInMemoryMemory
function createInMemoryMemory(): MemoryAdapter;Map-backed; no persistence. For tests and ephemeral runs.
Tools
rememberTool
function rememberTool(opts: {
adapter: MemoryAdapter;
agentId: string;
runId: string;
emit?: (event: HarnessEvent) => void;
}): ToolDef;Auto-injected by the harness when spec.memory.enabled is true and
deps.memory is wired. Emits memory_written on success.
recallTool
function recallTool(opts: {
adapter: MemoryAdapter;
agentId: string;
runId: string;
emit?: (event: HarnessEvent) => void;
defaultLimit?: number; // default 10
}): ToolDef;Default scopes inferred from context. Always filters agent-scope reads
to opts.agentId — no cross-agent leaks. Emits memory_recalled.
forgetTool
function forgetTool(opts: {
adapter: MemoryAdapter;
agentId: string;
enforceOwnership?: boolean; // default true
}): ToolDef;Only injected when spec.memory.allowForget === true. Refuses to
delete entries owned by a different agent unless
enforceOwnership: false.
memoryRateLimitPlugin
function memoryRateLimitPlugin(opts?: {
writesPerTurn?: number; // default 1
toolIds?: readonly string[]; // default ["remember"]
}): HarnessPlugin;Auto-applied by the harness when spec.memory.enabled and
writesPerTurn > 0. Counter resets on turn_start.
Instruction files
loadInstructionFiles
function loadInstructionFiles(opts: {
cwd: string; // bottom of the ancestor chain
filenames?: readonly string[]; // default ["AGENTS.md", "CLAUDE.md"]
root?: string; // default: nearest .git ancestor of cwd
maxBytesPerFile?: number; // default 16384
}): Promise<readonly InstructionFile[]>;
interface InstructionFile {
path: string; // absolute file path
dir: string; // absolute directory
relativeDir: string; // dir relative to root ("" at root)
filename: string; // bare filename
content: string; // body, truncated to maxBytesPerFile if oversized
truncated: boolean;
}Discovers instruction files along the ancestor chain from root down to
cwd. Returns root-first. Nothing is loaded unless called — no implicit
scan.
renderInstructionsBlock
function renderInstructionsBlock(files: readonly InstructionFile[]): string;Renders discovered files as a ## Project instructions system-prompt
block, one ### section per file in the given order. Empty string when
files is empty.
seedInstructionFiles
function seedInstructionFiles(
adapter: MemoryAdapter,
files: readonly InstructionFile[],
): Effect<void, MemoryError>;Upserts each file into the adapter as a global / reference entry
tagged instructions, with a stableId-derived id. Re-seeding the same
files is idempotent.
stableId
function stableId(seed: string): string;Deterministic, ULID-shaped (26-char Crockford base32) id derived from a seed string via SHA-256. Same seed → same id. Not time-sortable.
renderMemoryIndex
function renderMemoryIndex(
adapter: MemoryAdapter,
agentId: string,
opts?: { maxEntries?: number; maxPerScope?: number },
): Effect<string, never>;Builds the ## Recalled memory block appended to the system prompt
when spec.memory.includeIndex !== false. Returns empty string when
no entries match.
BM25
function bm25(query: string, docs: readonly Doc[]): ScoredDoc[];
function buildDoc(id: string, summary: string, body: string, tags: readonly string[] | undefined): Doc;
function tokenise(text: string): string[];Standard Okapi BM25 (k1=1.5, b=0.75) with a tag-match boost. Drops
~25 English stopwords and single-char tokens. No external deps.
MemoryError
class MemoryError extends Data.TaggedError("MemoryError")<{
op: "put" | "get" | "search" | "list" | "forget" | "close" | "init";
reason: "not_found" | "invalid_scope" | "path_escape" | "io" | "invalid_id";
message: string;
id?: string;
}> {}Spec integration
interface MemorySpec {
enabled: boolean;
includeIndex?: boolean; // default true
maxIndexEntries?: number; // default 30
allowForget?: boolean; // default false
writesPerTurn?: number; // default 1; 0 disables rate-limit
}
interface InstructionFilesConfig {
filenames?: readonly string[]; // default ["AGENTS.md", "CLAUDE.md"]
root?: string; // default: nearest .git ancestor of cwd
maxBytesPerFile?: number; // default 16384
seedMemory?: boolean; // default true
}spec.memory (a MemorySpec) and spec.instructionFiles (an
InstructionFilesConfig) both go into hashSpec — spec-drift on either
rejects resume with ResumeError. Instruction-file content is not
hashed, only the config.