@drover/memory

MemoryAdapter, in-memory and markdown impls, remember/recall/forget tools.

Implements the memory module — self-curated knowledge across runs.

MemoryEntry

ts
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

ts
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

ts
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

ts
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

ts
function createInMemoryMemory(): MemoryAdapter;

Map-backed; no persistence. For tests and ephemeral runs.

Tools

rememberTool

ts
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

ts
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

ts
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

ts
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

ts
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

ts
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

ts
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

ts
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

ts
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

ts
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

ts
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

ts
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.

Type to search…

↑↓ navigate open esc close