Understory: plain-markdown memory for AI agents, wired into a living graph
understory — memory that grows. Self-wiring, plain-markdown memory for AI agents (MCP + local models + a living graph).
At a glance
- What is it?
- Understory is an Apache-2.0 MCP server and web UI that files what your agents learn as markdown concept files in the Open Knowledge Format, cross-links them into a graph, and enforces bundle conformance in code rather than prompts. Any OpenAI-compatible or Anthropic-compatible endpoint can drive it, including a local llama.cpp server.
- Who is it for?
- Understory fits agent builders who want memory as portable, git-diffable markdown and are comfortable pointing it at their own LLM endpoint, including a local llama-server with the jinja flag for tool calling. It does not fit deployments that cannot spend an inference call per memory operation, since every tool call drives an internal agent, and it does not fit teams needing a battle-tested release line, because no releases are published yet.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 5 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 17, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
Memory that stays in your git repository
Agent memory usually ends up in a store you cannot read, diff or leave. Understory, by thecodacus, takes the opposite bet: it calls itself memory that grows, a self-wiring, plain-markdown memory layer beneath your agents. Every fact an agent learns is filed as a markdown concept, cross-linked into a living knowledge graph, and kept healthy by the agent itself. The README's pitch is that the result stays searchable, diffable and entirely yours, and that it runs well on local models.
The storage format is not invented here. Bundles follow the Open Knowledge Format (OKF) v0.1 spec: plain markdown files with YAML frontmatter, readable by humans, diffable in git, and portable across tools. That choice matters more than any feature list, because it means your agent's memory is a directory of text files you can back up, review in a pull request, or walk away with.
Three ways in, one agent
The system exposes one internal LLM agent through three doors. The first is an MCP server with five tools, memory_query, memory_add, memory_update, memory_status and memory_maintain, served over stdio or streamable HTTP, and each call drives that internal agent with the OKF spec in its system prompt. The second is a web UI: you browse the bundle as a tree with a concept viewer, an update log and a conformance badge, see the memory as an Obsidian-style force-directed graph that you can drag, pan and zoom, with nodes colored by type, sized by connections and orphans ringed in red, and chat with the same agent to test it while its tool calls render inline.
The third door is the one that rewards trust-building. Every agent run, whether a query, a mutation or a chat, records its traversal of searches, reads and writes as a compact notation under a .traces directory in the bundle. The graph view lists recent runs, and selecting one replays the path as numbered directed hops over the graph, with visited concepts ringed and search hits dotted. For a memory you are supposed to rely on, being able to watch exactly how an answer was assembled is the difference between a database and a black box.
Conformance is enforced in code, not prompts
The design rule the README states in bold terms is that conformance lives in code, not prompts. A deterministic bundle layer validates frontmatter, requiring the type field, regenerates index.md files, appends entries to log.md newest-first according to section 7 of the spec, and sandboxes every path to the bundle root.
The division of labour is the point. The LLM decides what to change; the code guarantees the result is a conformant bundle. An agent can hallucinate a file name or forget a frontmatter field, and the layer beneath it still refuses to produce a malformed bundle. For infrastructure meant to accumulate value over months of agent sessions, that is the load-bearing decision in the whole project.
Quick start with Docker
No clone is needed because the image is public. The README's compose file points at ghcr.io/thecodacus/understory:latest, exposes port 3800, mounts a volume at /bundle, and passes the provider configuration through environment variables, with BUNDLE_ROOT set to /bundle and an extra_hosts entry so the container can reach a llama.cpp server on the host. The first nine lines:
services:
understory:
image: ghcr.io/thecodacus/understory:latest
ports:
- "3800:3800"
# Lets the container reach a llama.cpp server running on the host via
# http://host.docker.internal:8080/v1 (see "Local llama.cpp" below).
extra_hosts:
- "host.docker.internal:host-gateway"Then bring the stack up:
docker compose up -dThe web UI appears at http://localhost:3800, and the MCP endpoint waits at http://localhost:3800/mcp over streamable HTTP. Registering it with an MCP client is one documented command:
claude mcp add --transport http ustory http://localhost:3800/mcpYour agent now has the five memory tools and, the README notes, receives a seed overview of the memory at every session start. The suggested first move is to teach it something with memory_add, the example being that deployments happen on Fridays and never Mondays, then open the graph and watch the concept wire itself in. Portainer users can load the repository's docker-compose.portainer.yml as a stack.
Providers: cloud APIs or a local llama-server
The provider system accepts any OpenAI-compatible or Anthropic-compatible API. You set LLM_API_BASE_URL, LLM_API_KEY and LLM_MODEL and leave LLM_PROVIDER unset; the format switch LLM_API_FORMAT takes openai or anthropic. The README gives one-line examples for DeepSeek, OpenAI, Anthropic and Groq, and a local setup that needs only the base URL:
LLM_API_BASE_URL=http://host.docker.internal:8080/v1 LLM_MODEL=One Docker detail trips people up, and the README calls it out: inside a container, localhost is the container itself, so a llama-server on the host is reached at host.docker.internal, which the compose file maps via extra_hosts; running from source on the same box, plain localhost works. An optional fallback chain uses the matching LLM_FALLBACK variables, so a local model can defer to a hosted one, and the old per-provider environment variables still work but are deprecated.
For llama.cpp specifically, the server must run with the jinja flag to enable OpenAI-style tool calling:
llama-server -m model.gguf --jinja --host 0.0.0.0 --port 8080No model id is needed in understory's configuration because it discovers the model for llama-server-like endpoints, and behind llama-swap the discovery prefers the currently loaded model, so a query does not trigger a multi-minute model swap; a specific model can be pinned with LLM_MODEL.
From source: three packages and a thin seam
The repository is a pnpm monorepo with three packages, and the split mirrors the design rule. packages/core holds the OKF bundle layer, which involves zero LLM calls, plus the agent loop built on the Vercel AI SDK with search, read, list, write, patch and delete tools, and the provider registry. packages/server is an Express app exposing MCP over streamable HTTP at /mcp, a stdio binary, a REST browse API, streaming chat, and the built web UI. packages/web is a Vite, React and TypeScript frontend with Tailwind for the bundle browser and the agent chat.
Running from source follows the README's block:
pnpm install
pnpm build
cp .env.example .env # add your API key
BUNDLE_ROOT=./sample-bundle \
LLM_API_BASE_URL=https://api.deepseek.com/v1 \
LLM_API_KEY=sk-... \
LLM_API_FORMAT=openai \
LLM_MODEL=deepseek-chat \
node packages/server/dist/index.jsThe server then serves the web UI, the REST API and the MCP endpoint on port 3800. A sample-bundle directory ships in the repository, which gives you something conformant to point BUNDLE_ROOT at before you have memories of your own.
What to weigh before adopting
The costs are as explicit as the benefits. Every memory operation drives an internal LLM agent, so each add, update or query consumes tokens against whatever provider you configured; the README's answer is that the system runs well on local models, and the fallback chain lets a local model carry routine traffic with a hosted one behind it. The bundle format is pinned to version 0.1 of the OKF spec, an early revision, so expect the format to evolve. And the repository has published no GitHub releases so far, with the last push on 2026-09-13, which makes pinning the image digest rather than the latest tag the careful choice.
None of these are hidden; they are the visible edges of a design that puts everything in plain files and makes the agent do the filing. If your agents need memory you can read, diff and take with you, the trade is a good one. If you need memory that never costs an inference call, this is not that project.
Editorial conclusion
Understory fits agent builders who want memory as portable, git-diffable markdown and are comfortable pointing it at their own LLM endpoint, including a local llama-server with the jinja flag for tool calling. It does not fit deployments that cannot spend an inference call per memory operation, since every tool call drives an internal agent, and it does not fit teams needing a battle-tested release line, because no releases are published yet. Before adopting, run docker compose up with the sample bundle, try one memory_add and replay its trace in the graph view, and pin your image digest. The last push was on 2026-09-13 under the Apache-2.0 licence.
Frequently asked questions
Does Understory need a cloud LLM?
No. The provider layer accepts any OpenAI-compatible or Anthropic-compatible endpoint, including a llama.cpp server on your own hardware, and an optional fallback provider can sit behind a local model.
Where does Understory store memories?
As plain markdown files with YAML frontmatter inside a bundle directory mounted at /bundle in Docker. The files follow the Open Knowledge Format, so they stay readable, diffable in git and portable across tools.
How do my agents talk to Understory?
Through five MCP tools, memory_query, memory_add, memory_update, memory_status and memory_maintain, exposed over stdio or streamable HTTP at the /mcp endpoint.
Community notes