Skip to content

feat(experimental): add context memory — persistent key-value blocks for agents#1005

Draft
mattzcarey wants to merge 2 commits intomainfrom
feat/context-memory
Draft

feat(experimental): add context memory — persistent key-value blocks for agents#1005
mattzcarey wants to merge 2 commits intomainfrom
feat/context-memory

Conversation

@mattzcarey
Copy link
Contributor

Summary

Adds Context Memory as the second tier of the experimental memory system (after Session Memory in #991). Context stores labeled text blocks that persist across sessions and can be injected into AI prompts.

Two primary use cases:

  • Readonly blocks — SOUL files, personality configs. Developer sets them, AI can read but never modify.
  • Writable blocks — todo lists, learned preferences. AI edits them via tool calls.

Usage

import { Context, AgentContextProvider } from "agents/experimental/memory/context";

// In your Agent class:
context = new Context(new AgentContextProvider(this), {
  blocks: [
    { label: "soul", description: "Agent personality", defaultContent: "You are helpful.", readonly: true },
    { label: "todos", description: "User's todo list", maxTokens: 5000 },
    { label: "preferences", description: "Learned user preferences", maxTokens: 2000 }
  ]
});

Reading blocks

context.getBlocks()                          // → Record<string, ContextBlock>
context.getBlock("soul")                     // → ContextBlock | null
context.toString()                           // → formatted text for system prompt injection

Writing blocks

context.setBlock("todos", "- Buy milk")      // upsert, returns ContextBlock
context.appendToBlock("preferences", "\n- likes dark mode")
context.deleteBlock("old_block")
context.clearBlocks()                        // clears all, re-inits defaults on next access

AI tool integration

const result = streamText({
  model: workersai("@cf/zai-org/glm-4.7-flash"),
  system: context.toString(),                // inject blocks into system prompt
  messages: [...],
  tools: {
    ...context.tools(),                      // AI gets update_context_block tool
    ...otherTools
  }
});

The tools() method returns a ToolSet (from AI SDK) containing an update_context_block tool. The tool description lists all writable blocks. Readonly blocks are excluded from the tool but visible in toString() output with readonly="true".

System prompt rendering

toString() renders all blocks as structured XML:

<context_block label="soul" description="Agent personality" readonly="true">
You are a helpful assistant.
</context_block>

<context_block label="todos" description="User's todo list">
- Buy groceries
- Walk the dog
</context_block>

Architecture

Mirrors the Session Memory pattern — wrapper class with business logic over a pure storage provider:

Context (business logic: readonly, maxTokens, defaults, tools, toString)
  └─ ContextProvider (interface — pure CRUD)
       └─ AgentContextProvider (DO SQLite)

Key design decisions

  • One block typeContextBlock used everywhere. Provider returns StoredBlock (Omit<ContextBlock, "tokens">), wrapper adds computed tokens field via estimateStringTokens().
  • Readonly enforced at wrapper levelsetBlock() and appendToBlock() throw on readonly blocks regardless of caller.
  • maxTokens uses token estimation — reuses the same estimateStringTokens() heuristic from Session Memory (hybrid char/word estimate, ~4 chars/token).
  • Predefined blocks with defaults — blocks from ContextOptions are lazily initialized on first read. Existing blocks are preserved.
  • System prompt not modified mid-turn — blocks are read at turn start and injected. AI edits via tool take effect on the next turn (preserves token caching).

Implementing custom providers

Implement the ContextProvider interface (5 methods):

interface ContextProvider {
  getBlocks(): Record<string, StoredBlock>;
  getBlock(label: string): StoredBlock | null;
  setBlock(label: string, content: string, metadata?: BlockMetadata): void;
  deleteBlock(label: string): void;
  clearBlocks(): void;
}

No business logic needed — the Context wrapper handles readonly, maxTokens, defaults, and token computation.

Limitations / reviewer notes

  • Token estimation is heuristicestimateStringTokens() uses ~4 chars/token (conservative). Not exact tokenizer counts. Fine for limits but not precision use cases.
  • TODO: enforce 2MB row limit — Agent SQLite has a 2MB row size limit. Not currently enforced in either Session or Context providers.
  • No per-block tooltools() returns a single update_context_block tool for all writable blocks. Could add per-block tools later if needed.
  • appendToBlock requires existing block — throws if block doesn't exist. Use setBlock for initial creation.

New files (7)

  • packages/agents/src/experimental/memory/context/types.ts — types
  • packages/agents/src/experimental/memory/context/provider.tsContextProvider interface
  • packages/agents/src/experimental/memory/context/context.tsContext wrapper class
  • packages/agents/src/experimental/memory/context/providers/agent.tsAgentContextProvider (DO SQLite)
  • packages/agents/src/experimental/memory/context/index.ts — barrel exports
  • packages/agents/src/tests/agents/context.ts — test agents
  • packages/agents/src/tests/experimental/memory/context/provider.test.ts — 20 tests

Tests

20 tests covering: CRUD, upsert, append, maxTokens enforcement (set + append), readonly enforcement (set + append), predefined blocks with defaults, predefined block re-initialization after clear, toString rendering (with/without readonly), empty state, and persistence across agent lookups.

Adds Context Memory as the second tier of the experimental memory system.
Context stores labeled text blocks (personality, preferences, tasks) that
persist across sessions and can be injected into AI prompts.

Key features:
- Readonly blocks (SOUL files the AI cannot modify)
- maxTokens enforcement via token estimation heuristic
- AI tool integration via tools() → ToolSet
- System prompt rendering via toString()
- Predefined blocks with defaults
@changeset-bot
Copy link

changeset-bot bot commented Feb 26, 2026

⚠️ No Changeset found

Latest commit: 4664c42

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 26, 2026

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@1005
npm i https://pkg.pr.new/cloudflare/agents/@cloudflare/ai-chat@1005
npm i https://pkg.pr.new/cloudflare/agents/@cloudflare/codemode@1005
npm i https://pkg.pr.new/cloudflare/agents/hono-agents@1005

commit: 4664c42

@msutkowski
Copy link
Contributor

Are you imagining a use case where this could work at two levels? It seems like there is a straightforward path to creating something like a UserMemoryDO that all agents belonging to a user could share common context (e.g. user preferences). I see the example of todos getting moved into a single Agent's context, while learned preferences would get lifted to the UserMemoryDO. Is this roughly the direction, or do you think there will be a different way to handle that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants