Skip to main content
Memory persists state across runs. Task outputs are per-run; memory is per-namespace and survives every workflow execution.
API reference: Memory lists every memory export and type, its options, and links to source and tests.

Four layers

import { createMemoryStore } from "smithers-orchestrator/memory";
import { Database } from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";

const sqlite = new Database("smithers.db");
const db = drizzle(sqlite);
const store = createMemoryStore(db);
const ns = { kind: "workflow" as const, id: "code-review" };
LayerAPIUse for
Factsstore.setFact(ns, key, value, ttlMs?) / store.getFact(ns, key) / store.listFacts(ns) / store.listAllFacts()Namespaced JSON facts. Optional TTL. Last-write-wins. listAllFacts() returns every fact across all namespaces (what memory list prints with no namespace).
Message historystore.createThread(ns, title?), store.listThreads(), store.saveMessage(msg), store.listMessages(threadId, limit), store.deleteMessages(threadId, messageIds)Ordered chat threads per agent or user; list/delete to compact history.
Notesstore.saveNote(input) / store.getNote(id) / store.listNotes(ns, filter?) / store.setNoteStatus(id, status) / store.enableNoteSearch(kind) / store.searchNotes(kind, query, limit?, filter?)Append-only knowledge (lessons, decisions, runbook rules). Notes never mutate; they die by supersession or rejection, not TTL. SQLite backend only.
Maintenancestore.deleteExpiredFacts() plus processorsTTL cleanup and history compaction.
Every method has an Effect-returning twin (store.listThreadsEffect, store.deleteMessagesEffect, etc.) for use inside an Effect pipeline.

Namespaces

type MemoryNamespace = { kind: "workflow" | "agent" | "user" | "global"; id: string };
Pick the kind to match the lifetime: workflow is scoped to a workflow definition; agent to an agent identity; user to an end user; global is shared across everything.

Task Memory Metadata

<Task
  id="review"
  output={outputs.review}
  agent={reviewer}
  memory={{
    recall:   { namespace: ns, topK: 3 },               // inject relevant past facts into prompt
    remember: { namespace: ns, key: "last-review" },    // persist this output as a fact
    threadId: `${ctx.input.repo}:reviews`,              // append messages to this thread
  }}
>
  Review the latest PR.
</Task>
memory is preserved on the task descriptor as metadata for runtimes and integrations that layer memory behavior onto task execution. The direct store APIs above are the current public memory read/write surface.

Imperative get/set/delete inside a workflow

The memory={{ recall, remember }} block above is declarative metadata; it is preserved on the task descriptor for runtimes that layer memory onto task execution, not an imperative call. To actually get, set, or delete a fact while a run is executing, build a store with createMemoryStore(db) and call it from inside a compute <Task> (a function task with no agent). The compute callback receives deps only; there is no injected store, so you create one:
import { createMemoryStore } from "smithers-orchestrator/memory";
import { Database } from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";

const store = createMemoryStore(drizzle(new Database("smithers.db")));
const ns = { kind: "workflow" as const, id: "code-review" };

<Task id="remember-model" output={outputs.done}>
  {async () => {
    await store.setFact(ns, "model", "gpt-4");           // set
    const fact = await store.getFact(ns, "model");        // get (MemoryFact | undefined)
    const all  = await store.listFacts(ns);               // list
    await store.deleteFact(ns, "stale-key");              // delete
    return { done: true };
  }}
</Task>
Create the store once at module scope (outside the workflow) and reuse it across tasks rather than re-opening the SQLite handle in every task body. Under Effect, the same operations are available as store.setFactEffect(ns, key, value, ttlMs?), store.getFactEffect(ns, key), etc., or via MemoryService (MemoryServiceApi), which also exposes the underlying .store.

Durable notes

Facts are a mutable scratch lane; notes are the durable knowledge lane. A note’s body, labels, and provenance never change after insert. Knowledge evolves by supersession: a new note lists the ids it replaces, and the superseded notes drop out of default reads only once the superseder is accepted. status is the one mutable field: a human or workflow gate flips it with setNoteStatus, so propose-then-reject leaves the original knowledge untouched. The default read contract (no filter) is a stability contract: reads return notes that are accepted and not superseded by an accepted note. Filters (status, includeSuperseded, kind, namespace) widen or narrow.
const ops = { kind: "user" as const, id: "ops-team" };

// Bank a lesson (append-only; provenance records which run learned it).
const lesson = await store.saveNote({
  namespace: ops,
  body: "Warm the cache tier before scaling deploys back up.",
  kind: "lesson",
  provenance: { runId: ctx.runId, nodeId: "bank" },
});

// Propose ONE rule that replaces piled-up lessons; pending hides nothing.
const rule = await store.saveNote({
  namespace: ops,
  body: "Always pre-warm caches after scale-down.",
  kind: "runbook-rule",
  status: "pending",
  supersedes: [lesson.id],
});

// The human gate: accepting the rule hides the lessons it replaces.
await store.setNoteStatus(rule.id, "accepted");

// Recall live knowledge only (accepted, not superseded).
const live = await store.listNotes(ops);
Full-text search is lazy and opt-in per namespace kind: nothing is indexed (and note writes pay nothing) until enableNoteSearch(kind), which creates the FTS index and backfills. searchNotes(kind, query) spans every namespace of the kind; pass { namespace } in the filter to stay namespace-local on a shared database. Notes require the SQLite backend and fail loud on Postgres/PGlite. See examples/incident-runbook-memory.jsx for the full recall → triage → bank → distill → ratify loop.

Processors

Maintenance jobs you run periodically:
import { TtlGarbageCollector, TokenLimiter, Summarizer } from "smithers-orchestrator/memory";

const gc         = TtlGarbageCollector();   // expire facts past their TTL
const limiter    = TokenLimiter(4000);      // keep history under token budget
const summarizer = Summarizer(myAgent);     // compress old messages with an LLM

await gc.process(store);
await limiter.process(store);
await summarizer.process(store);

Inspect from the CLI

bunx smithers-orchestrator memory list workflow:code-review -w workflow.tsx
The CLI currently exposes fact listing. Use the store API for writes, deletes, threads, messages, and TTL cleanup.

Notes

  • Memory and task outputs are distinct stores. Don’t use memory for run-scoped state; it’s not transactional with the workflow’s frame commits.
  • Working-memory writes are unordered. Use message history when sequence matters.