Skip to main content

Universal Memory Protocol (UMP)

A provider-agnostic document standard for agent memory and project rules. UMP files are plain Markdown with a YAML frontmatter header (ump_version, name, type, scope, visibility, provenance, links) and a free-form body. [[wikilink]]s written in the body are automatically discovered and merged into the document's links, so memory becomes a linkable graph rather than isolated files. The reference implementation ships an async reader/writer, a spec validator, and adapters that lift existing tool-specific memory files — CLAUDE.md, Cursor rules, Aider config, Continue config — into the shared format.

Import:

from synapsekit.ump import (
UMPDocument,
UMPFrontmatter,
UMPProvenance,
UMPReader,
UMPWriter,
UMPValidator,
ValidationError,
ValidationResult,
BaseUMPAdapter,
ClaudeAdapter,
CursorAdapter,
AiderAdapter,
ContinueAdapter,
auto_detect_and_convert,
)

No extra install is required — UMP is part of the core package (it uses PyYAML, already a core dependency).


Quickstart​

Read a UMP document from disk, inspect the wikilinks that were extracted from its body, then write it back out. File IO is async; the pure string transforms are synchronous.

import asyncio
from synapsekit.ump import UMPReader, UMPWriter, UMPValidator

MEMORY = """---
ump_version: "1.0"
name: testing-standards
type: feedback
scope: project
visibility: shared
provenance:
authors: ["human", "agent:claude"]
---

Always write pytest tests. See [[user-preferences]] and [[async-rules]].
"""

async def main():
# Parse from a raw string (sync) ...
doc = UMPReader.parse(MEMORY)

# ... wikilinks in the body are auto-discovered and merged into links:
print(doc.frontmatter.links) # ['[[user-preferences]]', '[[async-rules]]']
print(doc.frontmatter.type) # 'feedback'

# Validate against the UMP spec.
result = UMPValidator.validate(doc)
print(result.is_valid) # True

# Serialize back to a string (sync) ...
print(UMPWriter.serialize(doc))

# ... or persist to disk (async; creates parent dirs).
await UMPWriter.write_file(doc, "memory/testing-standards.ump.md")

# And read it back (async).
loaded = await UMPReader.read_file("memory/testing-standards.ump.md")
print(loaded.frontmatter.name) # 'testing-standards'

asyncio.run(main())

The document model​

A UMPDocument is a dataclass with three parts:

FieldTypeDescription
frontmatterUMPFrontmatterStructured YAML header
bodystrFree-form Markdown content
source_pathstrAbsolute path the document was read from (empty for in-memory docs)

UMPDocument supports to_dict(), from_dict(data), and to_json() for interchange.

UMPFrontmatter​

FieldTypeDefaultNotes
ump_versionstr"1.0"Spec version
namestr""Human-readable document name
typeUMPType"general"user / feedback / project / reference / general
scopeUMPScope"project"global / project / session
visibilityUMPVisibility"local"local / shared / team
provenanceUMPProvenanceemptyAuthors, evidence, signature
linkslist[str][]Wikilinks (e.g. "[[other-doc]]")

UMPProvenance​

FieldTypeDefault
authorslist[str][]
evidencelist[str][]
signed_bystr""

Empty provenance fields are omitted when serializing, keeping the YAML concise.


Reading and writing​

UMPReader and UMPWriter expose both sync string transforms and async file IO. The split is intentional and enforced by a test tripwire: string methods stay synchronous, file methods stay coroutines so they never block the event loop.

UMPReader​

MethodSync/AsyncDescription
UMPReader.parse(content, *, source_path="")syncParse a raw string into a UMPDocument
await UMPReader.read_file(path)asyncRead + parse a file (str | Path); sets source_path to the resolved path

parse splits the YAML frontmatter from the body, then scans the body for [[wikilink]] patterns. Discovered links are de-duplicated and merged into frontmatter.links (existing frontmatter links are preserved; new ones are appended). Malformed or missing frontmatter is tolerated — the parser falls back to defaults and the body always survives.

UMPWriter​

MethodSync/AsyncDescription
UMPWriter.serialize(doc)syncRender a UMPDocument to a UMP-formatted string
await UMPWriter.write_file(doc, path)asyncSerialize and write to disk, creating parent directories

Frontmatter is rendered with sort_keys=False (field order preserved) and allow_unicode=True.


Validation​

UMPValidator checks a document against the spec and returns a ValidationResult — it never raises during validation of a parsed document.

from synapsekit.ump import UMPValidator

result = await UMPValidator.validate_file("memory/testing-standards.ump.md")
if not result.is_valid:
for err in result.errors:
print(err.field, err.message) # e.g. type "Invalid type 'foo'. Must be one of [...]"
for warn in result.warnings:
print("warning:", warn.field, warn.message)
MethodSync/AsyncDescription
UMPValidator.validate(doc)syncValidate an in-memory UMPDocument
await UMPValidator.validate_file(path)asyncRead + validate a file; a read/parse failure yields an is_valid=False result with a field="file" error

A ValidationResult carries is_valid: bool, errors: list[ValidationError], and warnings: list[ValidationError]. Each ValidationError has field, message, and severity ("error" or "warning").

Errors (make the document invalid): an unrecognized type, scope, or visibility. Warnings (advisory only): a version other than 1.0, an empty name, an empty body, or provenance with no authors.


Adapters — import existing tool memory​

Adapters lift a tool's native memory/rules file into a UMPDocument so heterogeneous configs share one format. All adapters subclass BaseUMPAdapter and expose the same interface.

Adaptertool_namedefault_filenameBody handling
ClaudeAdapterclaude-codeCLAUDE.mdRaw Markdown
CursorAdaptercursor.cursorrulesRaw text; detect() also matches .cursor/rules
AiderAdapteraider.aider.conf.ymlYAML parsed to pretty JSON (falls back to raw on parse error)
ContinueAdaptercontinue.continue/config.jsonJSON pretty-printed (falls back to raw on parse error)

Each adapter provides:

  • Adapter.detect(base_dir) -> bool (sync) — whether the source file exists under a directory.
  • await Adapter.to_ump(path) -> UMPDocument (async) — read the source file and wrap it, stamping a sensible name (e.g. "claude-memory", "cursor-rules") and provenance.authors (e.g. ["agent:claude-code"]). A missing file yields an empty body rather than an error.
  • Adapter.from_ump(doc) -> str (sync) — the document body, for round-tripping back out.

Auto-detect a whole directory​

from synapsekit.ump import auto_detect_and_convert

# Scans for CLAUDE.md, .cursorrules/.cursor/rules, .aider.conf.yml,
# .continue/config.json and converts each one it finds.
docs = await auto_detect_and_convert("/path/to/project")
for doc in docs:
print(doc.frontmatter.name, "<-", doc.source_path)

auto_detect_and_convert(directory) (async) returns a list[UMPDocument], one per detected tool file — a one-call way to normalize a project's scattered agent-memory files into UMP.


API reference​

UMPReader​

  • parse(content: str, *, source_path: str = "") -> UMPDocument — sync
  • async read_file(path: str | Path) -> UMPDocument

UMPWriter​

  • serialize(doc: UMPDocument) -> str — sync
  • async write_file(doc: UMPDocument, path: str | Path) -> None

UMPValidator​

  • validate(doc: UMPDocument) -> ValidationResult — sync
  • async validate_file(path: str | Path) -> ValidationResult
  • Class constants: UMP_VERSION = "1.0", VALID_TYPES, VALID_SCOPES, VALID_VISIBILITIES

Adapters (BaseUMPAdapter subclasses)​

ClaudeAdapter, CursorAdapter, AiderAdapter, ContinueAdapter:

  • detect(base_dir: str | Path) -> bool — sync
  • async to_ump(path: str | Path) -> UMPDocument
  • from_ump(doc: UMPDocument) -> str — sync
  • async auto_detect_and_convert(directory: str | Path) -> list[UMPDocument]

Data types​

  • UMPDocument(frontmatter, body, source_path) — to_dict(), from_dict(), to_json()
  • UMPFrontmatter(ump_version, name, type, scope, visibility, provenance, links)
  • UMPProvenance(authors, evidence, signed_by)
  • ValidationResult(is_valid, errors, warnings)
  • ValidationError(field, message, severity)
  • Type aliases: UMPType, UMPScope, UMPVisibility

See also​