Skip to content

Memory

Kuralle agents have two complementary memory axes:

  • Working memory — small, durable, human-readable blocks (e.g. a USER profile) that the agent maintains with a tool and that are injected into the system prompt every turn. Persists across sessions. This is the right tool for “remember this caller’s name / preferences / account”.
  • Semantic recall — vector-retrieved facts extracted from past conversation (memory.preload / memory.ingest). Best for “search everything we’ve ever discussed”.

This guide focuses on working memory.

import { defineAgent, createRuntime } from '@kuralle-agents/core';
import { FilePersistentMemoryStore } from '@kuralle-agents/core';
const agent = defineAgent({
id: 'support',
model,
instructions: 'You are a helpful support agent.',
memory: {
workingMemory: {
store: new FilePersistentMemoryStore(), // where blocks persist
autoLoad: [{ scope: 'user', key: 'USER' }],
},
},
});

That’s it. When the user shares something durable (“my plan is Pro”, “call me Sam”), the agent calls the built-in memory_block tool to record it; on the next session for the same user, that block is loaded back into the prompt and the agent just knows it. You do not need to instruct the agent to use memory — Kuralle injects the directive automatically.

memory: {
workingMemory: {
store, // PersistentMemoryStore — required (see resolution below)
autoLoad: [{ scope: 'user', key: 'USER', template: '...' }],
defaultCharLimit: 10_000, // max chars per block (default 10_000)
scanForInjection: true, // reject prompt-injection in writes (default true)
},
}

| Option | Type | Default | Meaning | | --- | --- | --- | --- | | store | PersistentMemoryStore | resolved (see below) | Where blocks are persisted. | | autoLoad | WorkingMemoryBlockSpec[] | [{ scope: 'user', key: 'USER' }, { scope: 'agent', key: 'MEMORY' }] | Which blocks to load at session start and inject into the prompt. | | defaultCharLimit | number | 10_000 | Per-block character cap; writes over the limit are rejected. | | scanForInjection | boolean | true | Scan block writes for prompt-injection patterns and reject matches. |

Each entry declares one block to load and expose:

{
scope: 'user', // 'user' | 'agent' | 'shared'
key: 'USER', // the block name within its scope
template: '## About the user\n- name:\n- plan:', // optional seed (see below)
}
  • key — the block’s name. autoLoad: [{ scope: 'user', key: 'USER' }] exposes a single per-user block named USER. You can declare several (e.g. a USER profile plus a agent/MEMORY block).
  • template — optional starter content shown when the block is empty. It is not persisted on read — it only seeds the prompt so the model knows the shape to fill in, and is saved the first time the agent writes. Great for a structured profile (- name:\n- timezone:\n- plan:).

The store keys every block by (scope, owner, key). The owner is resolved automatically:

| scope | Owner | Use it for | | --- | --- | --- | | user | the request’s userId | Facts about the end-user (name, preferences, account). Shared across that user’s sessions. | | agent | the agent’s id | Knowledge global to the agent across all its users (house style, learned FAQs). | | shared | the request’s userId | A second per-user namespace, separate from the USER profile. |

When workingMemory is set, Kuralle does three things each turn:

  1. Loads the autoLoad blocks for the resolved owner and injects them as a ## Working memory section in the system prompt.
  2. Registers a memory_block tool (actions: view / add / replace / remove).
  3. Injects a directive telling the model to store durable facts proactively, answer from the blocks first, and not announce saves.

So the agent reads memory for free and writes it on its own — no extra prompt engineering required.

store resolves in this order: workingMemory.storeHarnessConfig.defaultWorkingMemoryStore → the Node file-store default (only if FilePersistentMemoryStore is imported) → an error.

| Store | Package | Persists to | Runs on | | --- | --- | --- | --- | | InMemoryPersistentMemoryStore | @kuralle-agents/core | process memory (ephemeral) | Node + Workers | | FilePersistentMemoryStore | @kuralle-agents/core | $KURALLE_MEMORY_DIR or ~/.kuralle/memories/<scope>/<owner>/<KEY>.md | Node | | PostgresPersistentMemoryStore | @kuralle-agents/postgres-store | a working_memory_blocks table | Node (Workers via Hyperdrive) | | RedisPersistentMemoryStore | @kuralle-agents/redis-store | wm:<scope>:<owner>:<key> keys | Node + Workers (Upstash REST) | | SqlPersistentMemoryStore | @kuralle-agents/cf-agent | the Durable Object’s embedded SQLite | Cloudflare Workers |

Route or layer backends with the composite stores in core:

import { RoutedPersistentMemoryStore, TieredPersistentMemoryStore } from '@kuralle-agents/core';
// Route by scope: per-user blocks in Postgres, per-agent in Redis.
const routed = new RoutedPersistentMemoryStore({
user: postgresStore,
agent: redisStore,
default: postgresStore,
});
// Or a read-through cache over a durable store.
const tiered = new TieredPersistentMemoryStore({ cache: inMemoryStore, durable: postgresStore });

The chat router reads userId from the request body and forwards it, so working memory is per-user automatically:

Terminal window
curl -X POST localhost:8787/api/chat \
-H 'content-type: application/json' \
-d '{ "message": "call me Sam", "sessionId": "s1", "userId": "user-42" }'

Pass a FilePersistentMemoryStore (or a Postgres/Redis store) to the agent, or set HarnessConfig.defaultWorkingMemoryStore once on createRuntime.

@kuralle-agents/cf-agent wires a SqlPersistentMemoryStore over the Durable Object’s embedded SQLite as the default working-memory store — zero config. Just set memory.workingMemory on your agent; blocks persist in the DO. userId comes from the request body.

The DO-SQLite store is scoped to the Durable Object instance. If you address one DO per conversation, a user-scope block only spans that conversation. To get true cross-session per-user memory on CF, either address the DO by userId (so a user’s sessions share one DO) or use an external store (PostgresPersistentMemoryStore / RedisPersistentMemoryStore via Upstash) so memory is shared across DOs.

For retrieval over everything ever discussed, enable memory.preload / memory.ingest with a MemoryService (RedisMemoryService, PostgresMemoryService, or InMemoryMemoryService). Working memory and semantic recall are independent — use both: working memory for the always-in-context profile, semantic recall for searching the long tail.