Skip to content

Memory ​

UGENT has a hybrid memory system that persists facts across conversations. Memory is scoped per workspace and optionally per user.

Enabling Memory ​

toml
[memory]
enabled = true
root_dir = "~/.ugent/workspace"
recall_cadence = "per_prompt"
  • recall_cadence = "per_prompt" — recall once per user message, reuse across iterations (recommended)
  • recall_cadence = "every_call" — recall every LLM call (legacy, higher token cost)

Backends ​

Full-Text Search (Default, No Embedding Model) ​

toml
[memory]
enabled = true
mode = "markdown_fts5"
backend = "legacy_markdown"

Uses Markdown files + SQLite FTS5 for lexical recall. No embedding model or API key needed. This is the default and works out of the box.

SQLite FTS Engine ​

toml
[memory]
enabled = true
mode = "workspace"
backend = "sqlite_fts"

New engine with reciprocal rank fusion. Still no embedding model required — pure lexical recall.

toml
[memory]
enabled = true
mode = "workspace"
backend = "lance_embedded"
embedding_dim = 1024

Requires building UGENT with the memory-lance cargo feature. Fuses vector search with FTS recall when an embedding model is configured:

toml
[memory.semantic]
provider = "openai_compatible"   # openai | openai_compatible | voyage | jina
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
model = "text-embedding-v4"
api_key = "$BAILIAN_API_KEY"

The embedding dimension must match the model's output width. If you change models, delete the LanceDB store.

Memory Tiers ​

Memories are organized into quality tiers:

TierDescriptionToken Budget
CoreAlways injected into the promptFixed
ContextualInjected when relevant to the current queryTunable
ArchivalRarely injected, compacted over timeMinimal
EphemeralSession-scoped, not persisted—

At most 15 core facts are injected into a turn. Recall injects identical texts once, as the best-ranked record, so a fact held in two recalled scopes — a user's own and the workspace's, say — takes one core slot, not two.

Recall Tuning ​

toml
[memory.search]
max_hits = 12
max_injected_tokens_est = 1200
  • max_hits — maximum number of memory entries recalled per turn
  • max_injected_tokens_est — approximate token budget for the injected [MEMORY] block

Citing What Memory Recalled ​

With an engine backend (sqlite_fts or lance_embedded), each fact recalled into a turn carries a citation marker, [[cite:mem:<record id>]]. An answer that cites a fact by its marker keeps the citation, and the web workspace shows it as Workspace memory, which has no file to open. A marker the agent wrote for a fact it was not given is dropped as invented. The default legacy_markdown backend writes no markers, so its facts are not cited.

Compaction ​

Scheduled compaction summarizes old conversations into concise facts:

toml
[memory.pruning]
enabled = true
per_actor_cap = 200

Per-actor pruning caps each user's memory count. Workspace and global scopes are never pruned.

Per-User Isolation ​

In multi-user deployments (web channel, federated identity), memory is isolated per user on the engine backends (sqlite_fts and lance_embedded):

  • Each user has their own memory scope
  • Workspace and global scopes are shared read-only context
  • Background extraction pulls durable facts after each turn without blocking the response
  • One user's fact never replaces another's. A record's id is derived from its text within its scope, so when two users state the same fact — "prefers concise answers" — each keeps a record of their own, and stating it again replaces only that scope's copy

On the default legacy_markdown backend, long-term memory is shared by the whole workspace, and the first channel turn from a named user logs a warning saying so.

Facts stored before ids were scoped keep their old ids until their scope writes them again, which replaces the old copy with the scoped one. Until then recall still injects each text once.

A person who reaches UGENT from more than one channel gets a separate memory scope on each, unless you link them:

toml
[[identity.links]]
actor = "owner"
providers = ["line:Uabc", "whatsapp:+15550100"]
  • A provider is channel:user. The channel matches without regard to case; the user id is taken as written, less any space around it.
  • Links are applied each time the agent starts. A provider listed twice is linked once, to the later entry's actor, and the log says so.
  • The file wins over a link UGENT already holds, such as one a federated sign-in made, and the log names the actor it replaced. A federated sign-in for that provider is refused from then on: a provider belongs to one actor.
  • Removing an entry does not unlink its providers. Delete the provider's row from the identity_links table in the sessions database, then restart.
  • A link moves memory, not a model choice. A user's /model choice stays with the identity they chose it under, so a linked person chooses on each channel.

Link to owner only what is yours

A provider linked to owner reads and writes the operator's own memory. A mistyped user id hands that memory to whoever owns the id.

Sub-Agent Inheritance ​

toml
[memory]
subagent_inheritance = "none"
  • none — sub-agents do not inherit parent memory (default)
  • full — sub-agents inherit parent memory context

Per-delegation inherit_memory = true on delegate_task can still opt in when set to none.

A worker that inherits memory reads and writes the memory of the user whose turn delegated the task, not the workspace's, and a parent resumed once its workers finish acts for that same user. A worker of a turn that kept out of memory — by the request's ugent_disable_memory_recall or ugent_disable_memory_write, or by its caller's defaults — inherits none, whatever this setting says. Work the runtime starts with no user to act for has no memory at all.

Write-Back ​

After each turn, UGENT extracts durable facts from the conversation in the background and stores them. This never blocks the response — extraction happens asynchronously.

On the engine backends, each write-back logs one line saying what it stored:

text
memory write-back extracted 2 facts, kept 2, stored 2 (session <id>, 840 ms)

extracted counts the facts the extraction model returned, kept those that passed validation, the sensitivity filter and deduplication, and stored those the store accepted. When something went wrong the line is a warning with the reason after the counts (... stored 0: <reason> (session <id>, 840 ms)): the model's error, an answer with no JSON facts or none that fits, or the store's refusal. A malformed fact is dropped alone, with a warning that names its position and shape, never its text.

Extraction runs on the default model unless you name another; a reasoning model works too, since its answer is read as its text, after the thinking:

toml
[memory.semantic.extraction]
provider_target = "openai:fast"   # provider:instance
model = "gpt-5.4-mini"

Import / Export ​

Startup does not import existing Markdown memory into an engine backend, and no command does it at present: facts in the legacy_markdown store stay there, and an engine backend does not recall them.

Released under the Private Beta License.