Skip to content

Persistent memory

Persistent memory lets the agent store and recall facts across sessions. Unlike session storage (which persists across HTTP requests from the same caller), persistent memory is project-scoped - facts saved in one REPL session are available in later sessions for the same project.

Persistent memory is primarily an agent mode concept, but the memory tools also work in workflow mode (see Workflow mode below). For how the agent decides what to remember and what to show the model each turn, see Memory internals.

How it works

Memory is stored in a bbolt (embedded key-value) database at ~/.kdeps/memory/<encoded-cwd>/memory.bolt. Each entry has a key, value, type, timestamps, and optional references to other entries for graph-based relationship tracking.

The memory store is injected into every LLM call automatically as a single graph-ordered <memory> block in the system prompt. Entries appear in causal order and the newest unfinished task is flagged, so a model resuming after an orchestrator model switch knows where to continue. See Memory internals for the block format.

Built-in memory tools

The agent has four LLM-callable tools for interacting with persistent memory:

ToolDescription
memory_saveSave a fact with a key and value. Keys should be short and descriptive.
memory_searchSearch entries by key or value (case-insensitive substring match).
memory_deleteRemove an entry by key.
memory_listList all stored keys (use memory_search to find content).

A fifth tool, memory_query, runs relational queries (select/project/join/union) over memory plus tool-call history and task state - see Relational query below.

memory_save

Creates or updates a memory entry. The entry is persisted immediately to disk.

json
{
  "name": "memory_save",
  "parameters": {
    "key": "project_name",
    "value": "kdeps - Go module github.com/kdeps/kdeps/v2"
  }
}

Finds entries where the key or value contains the query string (case-insensitive).

json
{
  "name": "memory_search",
  "parameters": {
    "query": "project"
  }
}

Returns matching entries as formatted text:

Found 2 memory entries:
- project_name: kdeps - Go module github.com/kdeps/kdeps/v2
- project_structure: Monorepo layout: cmd/, pkg/ (25 packages), docs/, tests/

memory_delete

Removes a single entry by key.

json
{
  "name": "memory_delete",
  "parameters": {
    "key": "stale_fact"
  }
}

memory_list

Returns all stored keys (no content). Use memory_search to find specific entries.

json
{
  "name": "memory_list"
}

Relational query (memory_query)

For filtering by field, combining facts across sources, or correlating past tool calls with the task that triggered them, memory_query runs a relational query - select/project/join/union - over three relations built from agent state:

RelationFieldsSource
memorykey, value, namespace, type, references, createdAt, updatedAtPersistent memory entries
tool_callsname, args, result, timestampRecent tool-call history (this session, most recent 200)
tasksid, desc, status, rounds, noteThe active goal's task list (empty when no goal is active)

The query language is expr-lang, the same engine before:/after: expressions use. filter()/map() are its own built-ins (select/project); join()/union() are added by memory_query:

OperationFunctionExample
Select (WHERE)filter(relation, predicate)filter(memory, .type == "error")
Project (columns)map(relation, expr)map(memory, {key: .key, value: .value})
Joinjoin(left, right, leftField, rightField)join(tool_calls, memory, "name", "key")
Unionunion(a, b)union(filter(memory, .type == "error"), filter(memory, .type == "decision"))

join is an equi-join, merging rows with left_/right_ prefixed field names so same-named fields never collide; no inequality/range-join or multi-field-key support today.

json
{"name": "memory_query", "parameters": {"query": "filter(memory, .type == \"error\")", "limit": 20}}

The result has rows (capped at limit, default 50, max 500), count (total matches before capping), and truncated (bool). memory_query is agent mode only - it reads the active Loop's state directly, so workflow mode has no LLM tool-call state to query.

Memory entry types

Entries are auto-classified by key pattern. The type controls where the entry sits in the memory graph and whether it can be pruned.

TypeKey patternsDescription
promptprompt, goal, taskUser goals and task descriptions
purposepurpose, why, reasonRationale for decisions
progressprogress, wip, in_progressWork in progress tracking
resultresult, output, doneCompleted work results
statusstatus, stateCurrent state information
tool_resulttool:*Tool call outputs (capped at 20)
thinkingthinking:*The model's reasoning text for a round, across every thinking-capable backend - searchable via memory_search. Capped at 20.
decisiondecision, decidedDesign decisions
preferencepreference, prefer, likeUser preferences
contextcontext, env, configEnvironment context
filefile, path, dir, last_filesFile references
actionlast_actionLast action taken
errorerror, fail, bugErrors and failures
note(default for unknown)Uncategorized entries
fact(not auto-assigned)General facts; grouped with note as low-signal (both capped at 50 combined)

Workflow mode

Memory tools work in both agent mode and workflow mode. In workflow mode, the store is lazy-initialized on first use via GetOrCreateMemoryStore(). No Loop required - memory is available to any resource or tool. memory_query is the exception: it needs the agent loop's live state and is agent mode only.

Configuration

Memory is enabled by default when the agent loop starts. No YAML configuration is needed. The store is created at ~/.kdeps/memory/<encoded-cwd>/memory.bolt where <encoded-cwd> is a sanitized version of the current working directory path.

See also

Released under the Apache 2.0 License.