Self-learning agent
Wire memory, see it surface a pre-seeded fact, write a new lesson.
End-to-end example: a writer agent with memory enabled, a pre-seeded
global fact, and a question that exercises both the auto-injected
index and the recall tool.
Define the agent
import { Type } from "@sinclair/typebox";
import { defineAgent } from "@drover/core";
export const writer = defineAgent({
id: "writer",
systemPrompt:
"You answer one question. If 'Recalled memory' appears in your " +
"system prompt, prefer those facts. Otherwise call `recall(query=...)` " +
"first. If you learn something worth keeping, call `remember`.",
inputSchema: Type.Object({ question: Type.String() }),
outputSchema: Type.Object({
answer: Type.String(),
used_memory: Type.Boolean(),
}),
model: "cheap",
tools: [],
quota: { maxTurns: 4 },
memory: {
enabled: true,
includeIndex: true,
writesPerTurn: 1,
},
});Wire memory + seed a fact
import { runAgent } from "@drover/facade";
import { createMarkdownMemory } from "@drover/memory";
import { Effect } from "effect";
import { writer } from "./writer.ts";
const memory = await createMarkdownMemory({ root: "./memory" });
// Seed one global fact the agent should surface.
await Effect.runPromise(
memory.put({
scope: "global",
kind: "reference",
summary: "drover uses TypeBox for all schemas",
body: "Tool I/O, agent specs, and MCP wire format all use TypeBox. Zod was rejected at design time.",
tags: ["schema", "drover"],
}),
);
const handle = runAgent(
writer,
{ question: "What schema library does drover use?" },
{ memory },
);
for await (const e of handle.events) {
if (e.kind === "memory_recalled") {
console.log(`recall q="${e.query ?? ""}" → ${e.hits.length} hit(s)`);
} else if (e.kind === "memory_written") {
console.log(`wrote ${e.entry.scope}/${e.entry.kind}: ${e.entry.summary}`);
}
}
const result = await handle.result;
console.log(result.output); // → { answer: "TypeBox.", used_memory: true }What happens on the first run
-
Harness builds the system prompt and appends:
## Recalled memory Scoped summaries — call `recall(query=..., scopes=...)` for the full body. ### Global - [reference] drover uses TypeBox for all schemas -
Model sees the seeded summary in turn 1 → answers
TypeBoxand setsused_memory: true. -
(Optional) If the model decides a follow-up lesson is worth keeping, it calls
remember(scope=..., kind=..., summary=..., body=...)— capped at one write per turn by the auto-applied rate-limit plugin.
What happens on the second run (same project)
The markdown adapter rebuilds its index on construction. Any memories the first run wrote land back in the index block of the second run’s system prompt automatically. No code changes needed.
Inspecting the store
tree memory/
# memory/
# ├── global/
# │ └── 01HXYZ...md # the seeded fact
# └── agents/
# └── writer/
# └── 01HXYZ...md # whatever turn 2 wroteEach file is YAML frontmatter + markdown body — diffable, grep-able, git-commit-able.
Inspecting on the timeline
The eval-viewer renders memory_written and memory_recalled events
inline on the run timeline. Drill into a run to see exactly which
memories the agent wrote vs surfaced.