Cover image for Durable, persistent memory for agents

anthony@make-it.run

Context windows make agents look stateful right up until the moment they need to resume tomorrow, survive a restart, or remember a user preference from last week. When facts, workflow progress, and approvals live only in the prompt, every new turn becomes a reconstruction job, and the failure is architectural rather than model-level.

The most useful design rule here is simple: keep authoritative state outside the prompt, and enforce memory scope in storage rather than in instructions. A prompt can ask the model to behave as if data belongs to one user or task, but that is only a hint. Durable memory gets safer and more reliable when the records themselves are explicitly typed, versioned, and namespaced by tenant, user, task, or agent.

That changes how we structure memory. The prompt becomes working memory for the current turn. Durable storage holds the source of truth. Retrieval is only a way to pull in the few records that matter now, not a substitute for proper state management. Once you make that separation, you stop trying to squeeze whole histories into every call and start deciding which state should exist independently of any one inference.

A practical TypeScript shape from the write-up looks like this:

type MemoryScope =
  | { kind: "user"; tenantId: string; userId: string }
  | { kind: "task"; tenantId: string; taskId: string }
  | { kind: "agent"; tenantId: string; agentId: string };

type MemoryRecord = {
  id: string;
  scope: MemoryScope;
  type: "episodic" | "semantic" | "procedural" | "state";
  content: string;
  attributes: Record<string, string | number | boolean>;
  provenance: {
    source: "user" | "tool" | "system";
    sessionId: string;
    messageId?: string;
    toolCallId?: string;
  };
  createdAt: string;
  version: number;
};

async function loadRelevantMemory(input: {
  scope: MemoryScope;
  query: string;
  recentMessages: string[];
}): Promise<MemoryRecord[]> {
  const exactState = await loadAuthoritativeState(input.scope);
  const retrieved = await hybridRetrieve({
    scope: input.scope,
    query: input.query,
    limit: 12,
  });

  return [...exactState, ...retrieved];
}

Enter fullscreen mode Exit fullscreen mode

There are two details worth copying directly. First, scope is part of the record, not an afterthought passed only to retrieval. That is what lets us enforce per-user or per-task boundaries in the database layer. Second, loadRelevantMemory loads authoritative state separately from retrieved memory. Exact state is the thing we trust; search results are supporting context.

The failure mode this prevents is subtle but common: a vector store quietly becomes the canonical database, and tenant isolation depends on prompt wording instead of storage keys. With this structure, we can keep stable workflow state, attach provenance to every write, version records over time, and still retrieve narrowly into the prompt. That gives the agent continuity without pretending the context window is a durable system of record.

The full write-up also covers:

  • why durable state belongs outside the context window
  • a four-layer architecture for production agent memory
  • state machines for long-running workflow memory
  • cost tradeoffs between long context and persistent memory
  • why memory writes need stricter controls than reads

Read the full article on make-it.run →