Skip to content

Long-term memory and task state APIs ​

中文 · Memory node contracts · Complete examples

Four capabilities share public MemoryStore/MemorySearchProvider contracts and Runtime Graphs. Switching relational or vector databases replaces application adapters, without private imports or database-specific Core nodes.

Complete invocation ​

Save this TypeScript example at the repository root and run node --env-file=.env your-example.ts with Node.js 24, a real model, Redis and the selected database. Package consumers copy the memory examples plus shared memory/storage tools, install Redis/pg application dependencies and configure the model. These helpers are application code, not Core package exports.

ts
import { mkdir, mkdtemp } from "node:fs/promises";
import { resolve } from "node:path";
import { createDitto, loadRuntimeConfigFile } from "@codesoul-co/ditto/runtime";
import { createInferWorker } from "@codesoul-co/ditto/worker/infer";
import { createInteractionWorker } from "@codesoul-co/ditto/worker/interaction";
import { openMemoryStorage } from "./examples/_shared/tools/memory/storage.ts";
import { memoryTools } from "./examples/_shared/tools/memory/adapters.ts";
import { namespace, type Backend } from "./examples/_shared/tools/memory/domain.ts";
import { createFixture } from "./examples/capabilities/memory/fixtures.ts";
import { sandbox } from "./examples/capabilities/memory/cli.ts";
import { writeMemories } from "./examples/capabilities/memory/shared.ts";
import { run } from "./examples/capabilities/memory/update.ts";

const config = loadRuntimeConfigFile("ditto.yaml", process.env);
const provider = config.model?.provider;
if (!provider || !config.providers[provider]) throw new Error("Configure a provider");
const model = config.providers[provider].model ?? config.model?.model;
if (!model) throw new Error("Configure a model");
const backend: Backend = "sqlite"; // "postgres" or "qdrant" uses the same workflow.
await mkdir(".examples-memory-tasks", { recursive: true });
const directory = await mkdtemp(resolve(".examples-memory-tasks/api-"));
const fixture = await createFixture(directory, "update", backend);
const storage = await openMemoryStorage({
  directory, backend, namespace: namespace(fixture.request), config,
});
try {
  const runtime = createDitto({
    config, sandbox: sandbox(config),
    workers: [...storage.workers, createInferWorker(),
      createInteractionWorker({ tools: memoryTools(directory, namespace(fixture.request)) })],
  });
  try {
    await writeMemories(runtime, fixture.memories);
    console.log(await run(runtime, { request: fixture.request, model: { provider, model } }));
  } finally { await runtime.close(); }
} finally { await storage.close(); }

Fixtures provide repeatable inputs. Business callers supply authenticated namespaces, explicitly approved preferences and actual project files, reusing the user's database for later tasks. run accepts {signal,stopAfter:"progress"} and returns a report or {status:"checkpoint"}.

Core wiring and operations ​

Register createMemoryWorker({store:databaseStore,search:optionalSearchProvider,concurrency:1}) from @codesoul-co/ditto/worker/memory. A store may implement its own native search; a separate search provider overrides it.

CapabilityNode inputBehavior
RecallMEMORY.SEARCH({query,strategy,filter,limit}){memory,score?}[]; SQL uses keyword, Qdrant uses vector
Read/restoreMEMORY.GET({ids?,keys?})Existing records only
PaginationMEMORY.QUERY({filter,limit,cursor?,orderBy?}){items,nextCursor?}; example adapters support ascending IDs
Policy-based writeMEMORY.WRITE({memories:[{key,content,metadata}]})Application validates permission/evidence; adapter handles idempotency and conflicts
Revise/augmentMEMORY.UPDATE({memories:[{id,content?,metadata?}]})Missing targets fail; omitted fields stay; provided metadata replaces the object
DeleteMEMORY.DELETE({ids})IDs actually removed; scope adapter prevents cross-user operations
CheckpointMEMORY.WRITE with a separate task keyCanonical request fingerprint, stage and intermediate results

Memory returns NodeResult. Check status === "success" before .output; backend errors are not an empty recall. Context returns raw Context and throws ContextError. These SQL/Qdrant adapters admit string filter fields key/namespace/kind. Filtering is a backend contract, not a universal Core query language. scopedMemory adds the trusted namespace, rejects forged keys/filters/metadata and checks ownership before ID mutations.

Relational and vector adapters ​

openSqliteMemory(path) uses file SQLite and transactions. openPostgresMemory(url,table) uses parameterized PostgreSQL queries, transactions and unique keys. The generic SQL adapter retains its MySQL dialect, but this task suite validates SQLite/PostgreSQL.

openQdrantMemory({url,collection,dimensions,embedding,embeddingIdentity,apiKey?}) stores complete records and supplies vector search. Preference content.text goes through public embedContents with an injected EmbeddingProvider; queries use the same model, then Qdrant performs native Cosine retrieval. UPDATE upserts new payload and regenerated vectors together. GET by key/ID needs no embedding. Checkpoints store payload with vector:{}, never fabricated semantic vectors.

Keys map to stable UUIDs. Writes reconcile full existing records; different values require UPDATE. Stable IDs do not replace a multi-writer transaction. A collection schema manifest fixes model identity, dimensions, preprocessing and distance; mismatches refuse startup. Reindex into a new collection when changing models.

Writes use Qdrant's documented wait=true completion confirmation; pagination uses scroll and offset. Backend HTTP routes and SDKs remain outside Core. Configure model, dimensions, endpoint and credentials together; vector storage alone does not embed text.

Durable protocol ​

  1. Read the real project file and relevant long-term memory; commit base.
  2. For mutations, infer a proposal, validate permission/evidence and commit candidate. Read current memory: matching operation ID/content means the mutation already took effect; otherwise WRITE/UPDATE only if the baseline still matches.
  3. Commit progress with intermediate results, memory snapshot and request fingerprint. Restore it after a pause or Redis expiry.
  4. Infer from restored Redis context, validate consistency with memory/progress, commit report, then publish the file.

There is no cross-service transaction between a mutation and its checkpoint. Uncommitted model steps may repeat after a crash; actual-record reconciliation avoids repeating a completed mutation. Committed reports do not repeat inference. An ongoing task uses its committed memory snapshot for reproducibility; new tasks recall current long-term memory. Hosts enforce authenticated identities and one active writing controller per user/task.

Ditto · @codesoul-co/ditto · Node.js 24+