Guides
Markdown memory files
Give each customer a short, versioned markdown file that your agents and your people maintain together — agreed terms, exceptions, processes, decisions. Agents read the outline, pull the one section they need, append under a heading, and review changes with diff and grep.
Semantic recall is great at finding relevant context in a pile of prose. It is the wrong shape for "what exactly did we agree with Acme?" — that answer should come from one curated page that everyone edits, not from whichever transcript passage ranks first. Memory files are that page: named markdown files scoped to an entity, with section-aware edits that never lose a concurrent write, a version history, unified diffs, and grep. Agents with small context windows can read a two-hundred-byte section instead of a whole file or a top-k of passages.
This guide covers the patterns. The Memory Files API reference has every parameter, status code, and limit.
Available with SDK 0.7.0
Memory files, readable document text and summary routes are available on the hosted API. Use version 0.7.0 or later in TypeScript (@aether-ai/sdk), Python (aether-ai), Go and .NET (AetherDb.Sdk). Self-hosted engines must enable memory files and summaries. Automatic summary generation requires a configured summarizer; user-written summaries do not.
Which memory to use
| You need to… | Use | Why |
|---|---|---|
| Find relevant context in lots of prose — notes, transcripts, documents | remember / recall | Semantic search ranks passages by meaning. |
| Look up current state — who works where, which plan a customer is on | Memory graph | Typed facts with history; a lookup, not a search. |
| Keep a short, curated document that people and agents both read and edit | Memory files | Read by section, edit by heading, versioned, diffable, greppable. |
| Work with a long PDF or contract without loading all of it | Summaries and document text | A few hundred words of gist, then one section at a time. |
They compose. A memory file's current version is indexed like any document, so recall and search can find it — and a search hit on a memory file tells you which section matched, so an agent can follow up with a section read.
One memory file per customer
The simplest durable pattern: one memory.md per customer, owned by an entity such as company:acme, with a fixed set of headings.
# Acme Corp
## Agreed terms
- 2026-09-01 — Net 30 payment terms (alice, renewal call)
## Exceptions
- Acme Ltd (UK) invoices in GBP (bob)
## Processes
- Escalations go to the shared account channel before email.
## Key decisions
- 2026-08-14 — Billing moves to annual invoicing from Q1 (carol)
A few conventions keep a file useful as it grows:
- One dated bullet per entry, with who recorded it and where it came from. Entries read well in a diff and grep cleanly.
- Stable headings. Agents address sections by heading text or slug (
agreed-terms), so don't rename them casually. - Small files. A file can hold up to 512 KiB, but every write stores a full new version. Split long-running topics into their own files (
onboarding.md,incidents.md) rather than growing one file forever. - One partition per end client. Memory files have no per-file reader lists — everyone in a workspace and partition can read them — so keep each end client in its own partition.
Create the file once
Create the file from a template with create-only semantics. If another worker created it first, the write fails with a version conflict, which you can ignore.
import os
from aether import AetherClient, Memory, VersionConflictError
client = AetherClient(api_key=os.environ["AETHER_API_KEY"])
memory = Memory("company:acme", client=client)
TEMPLATE = """# Acme Corp
## Agreed terms
## Exceptions
## Processes
## Key decisions
"""
try:
memory.files.write("memory.md", TEMPLATE, create=True)
except VersionConflictError:
pass # already created by someone else
The template is optional: append_section creates both a missing file and a missing heading for you. Creating it up front just fixes the section order.
Record an entry
Use append_section to add an entry under a heading. It needs no version check: Aether applies it atomically to the current version, so two agents recording entries at the same moment both land, and the SDK's automatic idempotency key makes a retried call safe. The entry goes at the end of that section's own text, ahead of any sub-headings. Pass author to record who the entry is for — for example the end user an agent is acting on behalf of.
memory.files.append_section(
"memory.md",
"Agreed terms",
"- 2026-10-10 — Net 45 for EU entities (bob, renewal call)",
author="user:bob",
)
If the heading doesn't exist yet, it is created at the end of the file (as ## by default). If a selector matches more than one heading, the call fails with 409 memory_file_section_ambiguous and lists the candidates — retry with the slug.
When entries come from a queue or a job that can redeliver the same event, pass your own idempotency_key (for example crm-note-<id>) so a redelivered event can't record the entry twice — see Idempotency keys.
Rewrite a section
Editing an existing entry — rather than adding one — replaces the section, so guard it with the version you read. On a version conflict, re-read and apply your change again.
current = memory.files.read("memory.md", section="processes")
updated = current.content.replace("before email", "before email or phone")
body = updated.split("\n", 1)[1].rstrip() # drop the heading; it is kept automatically
memory.files.replace_section(
"memory.md", "processes", body, expected_version=current.version
)
A section read includes its heading line and any blank lines before the next heading; replace_section keeps both, so send only the trimmed body. The read-modify-write recipe shows the retry loop in full.
Small-context agents: outline, then section
An agent with a few thousand tokens to spare shouldn't load a whole file to answer a question about payment terms. Every read returns the file's outline — each heading with its slug, line range, and size in bytes — so the agent can decide what to fetch:
- Read the outline (
outline=True): one small response. - Pick sections by slug and read only those (
section="agreed-terms"). - If a section is still too big, page it with
max_bytesandoffset; pages break on line boundaries.
def company_context(company: str, wanted: list[str], budget: int = 4000) -> str:
"""Return the wanted sections of a company's memory file, within a byte budget."""
memory = Memory(f"company:{company}", client=client)
outline = memory.files.read("memory.md", outline=True)
if outline.bytes <= budget:
return memory.files.read("memory.md").content # small enough: take it all
parts, used = [], 0
for s in outline.sections:
if s.slug in wanted and used + s.bytes <= budget:
parts.append(memory.files.read("memory.md", section=s.slug).content)
used += s.bytes
return "\n".join(parts)
context = company_context("acme", ["agreed-terms", "exceptions"])
Sections nest, so selecting a ## heading includes its ### sub-headings; an outline's bytes already counts them.
As agent tools
If your agent calls tools, a handful of narrow ones map directly onto these calls and keep each response small:
| Tool | Call | Returns |
|---|---|---|
memory_outline(company) | read(name, outline=True) | Headings, slugs, and sizes. |
memory_read_section(company, section) | read(name, section=…) | One section's markdown. |
memory_record(company, section, entry) | append_section(name, section, entry, author=…) | The new version. |
memory_search(pattern) | grep_memory_files(pattern, entity_prefix="company:") | Matching lines with file, line number, and section. |
Keep rewrites (write, replace_section) behind a human review step or a dedicated tool that always passes expected_version.
Review: history, diffs, and grep
Every change is a new version that records the operation, its author, and the authenticated principal. Use the history to see who changed what, a diff to review a change, and grep to answer exact questions across every customer at once.
history = memory.files.versions("memory.md", limit=10)
for v in history.versions:
print(v.version, v.op, v.author or v.principal)
# The most recent change, as a unified diff
print(memory.files.diff("memory.md").diff)
# Every company that invoices in GBP
hits = client.grep_memory_files("GBP", entity_prefix="company:")
for m in hits.matches:
print(m.entity_id, m.section, m.text)
A diff's from defaults to the version before to, and to to the current version; pass both to review a longer span — for example everything since last week's review. Grep is literal by default; set regex for a linear-time regular expression and ignore_case for case-insensitive matching. A scan stops at 1,000 matches, 500 files, 8 MiB, or 2 seconds and says so in truncated_reason, so narrow it with an entity, a prefix, or a file list on large workspaces.
The last 100 versions of each file are kept. For an erasure request, delete the file with hard=True to remove every version permanently.
Reading a long PDF: summary first, then sections
Summaries and document text give an agent the same outline-then-section workflow for documents you didn't write, like a 40-page contract:
- Insert with
summarize. Aether parses the PDF and, after the document is stored, writes a short markdown summary with fixed headings — Summary, Parties & dates, Terms & obligations, Exceptions, Open questions. - Read the summary. It is a few hundred words: enough to decide whether the document matters and which part to read.
- Read the outline of the document's text, then one section at a time.
- Record what you concluded in the customer's memory file, with a pointer back to the document.
import time
job = client.insert_async(
file_path="./acme-msa.pdf",
content_type="application/pdf",
tags=["company:acme"],
summarize=True,
)
done = client.wait_for_job(job["job_id"])
doc_id = done["doc_id"]
summary = client.get_summary(doc_id)
while summary.state == "pending":
time.sleep(2)
summary = client.get_summary(doc_id)
outline = client.get_text(doc_id, outline=True)
for s in outline.sections:
print(s.slug, s.bytes)
termination = client.get_text(doc_id, section="termination")
memory.files.append_section(
"memory.md",
"Agreed terms",
f"- 2026-10-10 — Either party may terminate on 90 days' notice (MSA, doc {doc_id})",
author="agent:contracts",
)
Things to know:
- Summaries are generated by Aether and editable. Treat a generated summary as a reading aid and check important details against the text. When a person corrects one, store it with
put_summary— it is kept as user-authored, and a later regeneration replaces it only withforce. - Text from PDFs is rebuilt from the parsed passages. It is best effort: whitespace at passage boundaries can differ from the original layout. Headings come through as markdown headings, which is what makes
outlineandsectionwork. - A summary follows its document. Updating the document regenerates an Aether-authored summary (or marks it
stale); deleting the document deletes the summary. - Short documents are skipped automatically — below 1,500 characters by default. An explicit
regenerate_summarystill works.
See Reading document text and Document summaries for the full reference.
Content the edge firewall may reject
The API's edge firewall may reject a request body that contains a path-traversal sequence such as ../../, or a link whose host is a raw IP address. The response is a 403 with a non-JSON body. Customer notes pasted into a memory file are the usual way to hit this: if your agents store arbitrary pasted text, rewrite such content before writing — link to a hostname instead of an IP address, or describe a relative path in words — and treat a non-JSON 403 on a write as rejected content, not an authentication failure. See Troubleshooting.
Next steps
- Memory Files API — every route, header, status code, and limit.
- Documents API: text and summaries — paging, sections, summary states, and workspace defaults.
- Memory graph — structured facts for state you look up rather than read.
- Multi-tenant patterns — keep each end client's files in its own partition.