How-to guides / Memory and conversations
Start here
How-to guides
How-to guides
Memory and conversations
An agent remembers what is in its conversation: the messages of earlier runs in the same conversation are sent to the model with the new input (oldest turns are dropped first when they no longer fit the context budget). Which conversation a run joins depends on how it was started.
Fleet
| How the work arrives | Conversation |
|---|---|
Task without a conversation key (agen run, SubmitTask, a trigger) on a pool or task deployment |
A new one for each task. |
| Task with a conversation key | One per key and deployment: every task with the same key continues it. |
Any task or call to a singleton |
The deployment’s one long-lived conversation (keys and contextId are ignored). |
A2A message/send to a pool or task deployment |
One per contextId and caller. A message without contextId gets a new one, returned in the answer; send it with the next message to continue. |
A delegated call (call_agent) |
A new one for each call. |
A task started with start_agent |
A new one for the task; its result comes back as a task in the caller’s conversation (see Connecting agents). |
Set the key per task:
agen run support "My order hasn't arrived" --conversation ticket-4711
agen run support "It was order 1234" --conversation ticket-4711
or with conversationKey on SubmitTask, or per webhook trigger from a header
or body field (see Triggers).
- Tasks of one conversation run one at a time, in the order they were submitted, so the history stays consistent. Different keys run in parallel.
- Keys are scoped by where they come from (API, each webhook trigger, each
A2A caller), so one source can never read another’s conversation. A task
shows its key with that scope (
api:ticket-4711); sending that value back toSubmitTaskcontinues the same conversation. - A run that is still unfinished blocks its conversation. If its task is cancelled or fails, the run is closed so the next task can start.
Queue modes for a busy conversation
“One at a time, in order” (above) is followup, a deployment’s default.
x-agen/config.json’s conversations.queueMode changes it:
{ "conversations": { "queueMode": "collect", "debounce": "2s" } }
collect: a task submitted while an earlier one of the same conversation is still queued (not yet leased) is merged into it instead of queuing separately — its input is appended, and the merged task’snot_beforemovesdebounce(default 2s) further out, so a burst of quick messages (someone typing, a flaky webhook retried) becomes one run instead of several back-to-back ones.SubmitTask’s response is the task it merged into, not a new one; keep using that id.debounceonly resets while the task is still queued — once it is leased, the next submission starts a fresh one.- Not yet implemented:
steerandinterruptas queue modes. The engine side both would need — a run’srun_inboxdrained into it before its next model call — now exists (it is what makessignal_agentwork), but neither mode’s ownSubmitTask→run_inboxwiring, norinterrupt’s “running run gets a last report turn first,” is built. Also missing:SubmitTask.deliveryto ask for a mode per task rather than per deployment, andcollectis not idempotency-key aware — a merged submission’s own key is not recorded, so retrying it with the same key merges again rather than returning the earlier result.
Labels
Labels are not memory, but they group work the same way you might use a
conversation key: --label project=apollo (or labels on SubmitTask, A2A
metadata agen.labels, or a trigger) is copied to the task’s runs, traces,
tool calls (_meta) and delegated tasks. Up to 32 labels; keys of letters,
digits and ._/-.
Embedded
With an SDK, one Agent object is one conversation: each run continues it.
Pass a session id to continue a stored conversation later, or ask for a new
conversation; see the SDK README for the exact options.
What is stored
Sessions, conversations, messages, runs and traces live in the Store
(SQLite locally, Postgres in a distributed fleet). Secrets are redacted before
anything is stored. There is no automatic summarisation of long histories yet:
keep long-lived conversations focused, or start a new key. Within one long
run, the oldest tool results are shortened in what is sent to the model once
the run outgrows contextTokens; the Store keeps them whole (see the
bundle reference).
Memory blocks
A conversation’s history is per-conversation; a memory block is durable, named text that outlives any one conversation and that an agent can update on its own — a running summary, a user’s preferences, a project’s open questions. Blocks are namespace-scoped, so a namespace’s deployments can share one.
Attach one in x-agen/config.json:
{ "memory": [{ "block": "user-prefs", "mode": "write" }] }
modegates what the agent may do with it:read(see it only, the default),append(add to it), orwrite(also replace it or edit it in place).- An attached block’s current value is rendered into the system prompt as
## Memory: <label> (<description>); one never written yet renders as nothing. - With at least one non-
readattachment, the agent gets amemory_updatetool:opreplace(the giventext),append(addstext), orstr_replace(old, which must occur exactly once in the block, becomesnew). PassexpectedVersion(the block’s last-seen version) to refuse the update instead of silently overwriting a concurrent change from another conversation.
People manage blocks directly, without an agent:
printf %s "Prefers metric units; working in Go" | agen memory set user-prefs --title "User prefs"
agen memory ls
agen memory set (SetMemoryBlock) always replaces a block’s value
outright and bumps its version; it is for people, not for an agent’s own
updates (which go through memory_update so a stale read cannot clobber a
newer write).