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

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

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

  1. 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
    
  2. Model sees the seeded summary in turn 1 → answers TypeBox and sets used_memory: true.

  3. (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

bash
tree memory/
# memory/
# ├── global/
# │   └── 01HXYZ...md         # the seeded fact
# └── agents/
#     └── writer/
#         └── 01HXYZ...md     # whatever turn 2 wrote

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

Type to search…

↑↓ navigate open esc close