Skip to content

Memory API reference

Complete reference for Astrocyte’s core memory operations — retain, recall, reflect, forget, history, audit, compile, graph, import, and export — via Python library and REST gateway.


OperationPurposePython methodHTTP endpoint
retainStore content into memoryastrocyte.retain()POST /v1/retain
recallRetrieve relevant memoriesastrocyte.recall()POST /v1/recall
reflectSynthesize an answer from memoryastrocyte.reflect()POST /v1/reflect
forgetRemove memoriesastrocyte.forget()POST /v1/forget
historyRecall memories as they existed at a past point in timeastrocyte.history()POST /v1/history
auditLLM-judged coverage analysis for a memory bankastrocyte.audit()POST /v1/audit
compileBuild durable wiki pages from retained memoriesastrocyte.compile()POST /v1/compile
graph searchSearch entities in the graph storeastrocyte.graph_search()POST /v1/graph/search
graph neighborsTraverse graph-linked memoriesastrocyte.graph_neighbors()POST /v1/graph/neighbors
export/importMove bank contents via AMA JSONLastrocyte.export_bank() / astrocyte.import_bank()POST /v1/export, POST /v1/import
create_directiveAuthor a user-curated hard ruleastrocyte.create_directive()MCP: memory_create_directive
list/create/delete observationCRUD for live observations with trend trackingastrocyte.list_observations() etc.MCP: memory_list_observations etc.
list/create/update/delete mental modelCRUD for curated structured summariesastrocyte.list_mental_models() etc.MCP: memory_list_mental_models etc.

Ingests content, extracts facts, deduplicates, and stores into a memory bank.

async def retain(
self,
content: str,
bank_id: str,
*,
metadata: dict[str, str | int | float | bool | None] | None = None,
tags: list[str] | None = None,
context: AstrocyteContext | None = None,
content_type: str = "text",
extraction_profile: str | None = None,
occurred_at: datetime | None = None,
source: str | None = None,
pii_detected: bool = False,
) -> RetainResult:
ParameterTypeDescription
contentstrThe text content to store.
bank_idstrTarget memory bank identifier.
metadatadictOptional key-value pairs attached to the memory.
tagslist[str]Optional tags for categorisation and filtering.
contextAstrocyteContextOptional actor identity and session context.
content_typestrContent format hint. Defaults to "text".
extraction_profilestrNamed extraction profile to control fact extraction behaviour.
occurred_atdatetimeTimestamp of when the content originally occurred.
sourcestrFree-text source label (e.g. "slack", "meeting-notes").
pii_detectedboolFlag indicating the content contains PII. Defaults to False.
FieldTypeDescription
storedboolWhether the content was stored.
memory_idstr | NoneID of the created memory, if stored.
deduplicatedboolWhether the content was merged with an existing memory.
errorstr | NoneError message if the operation failed.
retention_actionstr | NoneAction taken by the retention pipeline (e.g. "created", "merged").
curatedboolWhether curation rules were applied.
memory_layerstr | NoneLayer the memory was stored in (e.g. "episodic", "semantic").
from astrocyte import Astrocyte
ast = Astrocyte.from_config("astrocyte.yaml")
result = await ast.retain(
content="Customer prefers dark-mode UI and weekly email digests.",
bank_id="user-prefs",
tags=["ui", "notifications"],
metadata={"customer_id": "cust_8291"},
source="support-ticket",
)
print(result.memory_id) # "mem_3f8a..."
print(result.deduplicated) # False
POST /v1/retain
Content-Type: application/json
Authorization: Bearer <token>
{
"content": "Customer prefers dark-mode UI and weekly email digests.",
"bank_id": "user-prefs",
"metadata": {"customer_id": "cust_8291"},
"tags": ["ui", "notifications"]
}
Terminal window
curl -X POST https://gateway.example.com/v1/retain \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{
"content": "Customer prefers dark-mode UI and weekly email digests.",
"bank_id": "user-prefs",
"tags": ["ui", "notifications"],
"metadata": {"customer_id": "cust_8291"}
}'

Searches one or more memory banks and returns ranked hits.

async def recall(
self,
query: str,
bank_id: str | None = None,
*,
banks: list[str] | None = None,
strategy: Literal["cascade", "parallel", "first_match"] | MultiBankStrategy | None = None,
max_results: int = 10,
max_tokens: int | None = None,
tags: list[str] | None = None,
context: AstrocyteContext | None = None,
external_context: list[MemoryHit] | None = None,
fact_types: list[str] | None = None,
time_range: tuple[datetime, datetime] | None = None,
include_sources: bool = False,
layer_weights: dict[str, float] | None = None,
detail_level: str | None = None,
as_of: datetime | None = None,
) -> RecallResult:
ParameterTypeDescription
querystrNatural-language search query.
bank_idstrSingle bank to search. Mutually exclusive with banks.
bankslist[str]Multiple banks to search.
strategystr | MultiBankStrategyMulti-bank search strategy: "cascade", "parallel", or "first_match".
max_resultsintMaximum number of hits to return. Defaults to 10.
max_tokensintToken budget for returned content.
tagslist[str]Filter results to memories matching these tags.
contextAstrocyteContextOptional actor identity and session context.
external_contextlist[MemoryHit]Additional context to blend into results.
fact_typeslist[str]Filter to specific fact types (e.g. ["preference", "event"]).
time_rangetuple[datetime, datetime]Restrict results to a time window.
include_sourcesboolInclude source metadata in each hit. Defaults to False.
layer_weightsdict[str, float]Per-layer scoring weights (e.g. {"semantic": 1.2, "episodic": 0.8}).
detail_levelstrControls verbosity of returned text.
as_ofdatetime | NoneTime-travel filter. When set, only memories whose retained_at is on or before this UTC timestamp are returned. Defaults to None (no filter). See also history().
FieldTypeDescription
hitslist[MemoryHit]Ranked list of matching memories.
total_availableintTotal matches before max_results truncation.
truncatedboolWhether results were truncated.
traceRecallTrace | NoneDiagnostic trace of the recall pipeline.
authority_contextstr | NoneAuthority resolution context, if applicable.

RecallTrace is the per-query diagnostic surface — what strategies ran, how long each took, and which tier resolved the query. Useful for benchmark analysis, latency debugging, and observability dashboards.

FieldTypeDescription
strategies_usedlist[str] | NoneOrdered list of strategies that contributed candidates (e.g. ["semantic", "keyword", "graph", "temporal"]).
total_candidatesint | NoneTotal candidates seen across all strategies, pre-fusion.
fusion_methodstr | None"rrf" for reciprocal-rank fusion; future fusion algorithms set their own label.
latency_msfloat | NoneEnd-to-end recall latency, in milliseconds.
strategy_timings_msdict[str, float] | NonePer-strategy latency breakdown — useful when one arm dominates the budget.
strategy_candidate_countsdict[str, int] | NonePer-strategy candidate count contributed to fusion.
tier_usedint | NoneWhich retrieval tier resolved the query (1 for cache, 2 for fast path, etc.).
layer_distributiondict[str, int] | NoneHit count by memory layer — e.g. {"fact": 5, "observation": 3, "model": 1}. Visible when observation consolidation or mental models are enabled.
cache_hitbool | NoneTrue when the recall cache satisfied the query without running retrieval strategies.
wiki_tier_usedbool | NoneTrue when the wiki tier (M8 W5) resolved the query (compiled wiki page surfaced before raw recall).
FieldTypeDescription
memory_idstrUnique memory identifier.
textstrMemory content.
scorefloatRelevance score.
bank_idstrBank the memory belongs to.
metadatadictAttached metadata.
tagslist[str]Tags on the memory.
occurred_atdatetime | NoneWhen the content originally occurred (domain time, caller-supplied).
retained_atdatetime | NoneUTC wall-clock time when this memory was stored. Populated by the retain pipeline; use with as_of for time-travel queries.
sourcestr | NoneSource label.
memory_layerstr | NoneLayer (e.g. "episodic", "semantic").

Single bank:

result = await ast.recall("What UI theme does the customer prefer?", bank_id="user-prefs")
for hit in result.hits:
print(f"[{hit.score:.2f}] {hit.text}")

Multi-bank parallel:

result = await ast.recall(
"deployment issues last week",
banks=["incidents", "runbooks", "team-notes"],
strategy="parallel",
max_results=20,
)

Multi-bank cascade (searches banks in order, stops when enough results are found):

result = await ast.recall(
"API rate limit policy",
banks=["policies", "runbooks", "general"],
strategy="cascade",
)

Tag filtering:

result = await ast.recall(
"onboarding steps",
bank_id="docs",
tags=["onboarding", "getting-started"],
)

Time range filtering:

from datetime import datetime, timedelta
week_ago = datetime.now() - timedelta(days=7)
result = await ast.recall(
"customer complaints",
bank_id="support",
time_range=(week_ago, datetime.now()),
)
Terminal window
curl -X POST https://gateway.example.com/v1/recall \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{
"query": "What UI theme does the customer prefer?",
"bank_id": "user-prefs",
"max_results": 5
}'

Multi-bank:

Terminal window
curl -X POST https://gateway.example.com/v1/recall \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{
"query": "deployment issues last week",
"banks": ["incidents", "runbooks", "team-notes"],
"strategy": "parallel",
"max_results": 20
}'

history() — Recall a point-in-time snapshot

Section titled “history() — Recall a point-in-time snapshot”

Returns the memories that existed in a bank at a specific past moment — only memories whose retained_at timestamp is on or before as_of are included. Use this for post-mortems, compliance audits, and debugging (“what did the agent believe on date X?”).

For most use cases history() is the right call. If you need finer control (multi-bank queries, layer weights, etc.) pass as_of directly to recall() instead.

async def history(
self,
query: str,
bank_id: str,
as_of: datetime,
*,
max_results: int = 10,
max_tokens: int | None = None,
tags: list[str] | None = None,
) -> HistoryResult:
ParameterTypeDescription
querystrNatural-language search query to run against the historical snapshot.
bank_idstrBank to query.
as_ofdatetimeUTC timestamp. Memories retained after this moment are excluded. Must be timezone-aware.
max_resultsintMaximum number of hits to return. Defaults to 10.
max_tokensintOptional token budget for the result set.
tagslist[str]Optional tag filter applied on top of the time filter.
FieldTypeDescription
hitslist[MemoryHit]Ranked memories visible at as_of.
total_availableintTotal matches before max_results truncation.
truncatedboolWhether results were truncated.
as_ofdatetimeThe timestamp used for the snapshot query (echoed back for traceability).
bank_idstrThe bank that was queried.
traceRecallTrace | NoneDiagnostic trace of the recall pipeline.

Each MemoryHit in hits includes a retained_at field showing exactly when that memory was stored.

Post-mortem — what did the agent know on 1 Jan 2025?

from datetime import datetime, UTC
snapshot = await ast.history(
"What did we know about Alice's employer?",
bank_id="user-alice",
as_of=datetime(2025, 1, 1, tzinfo=UTC),
)
print(f"Snapshot at: {snapshot.as_of}")
for hit in snapshot.hits:
print(f" retained={hit.retained_at:%Y-%m-%d} {hit.text}")

Compliance audit — memory state before a policy change:

from datetime import datetime, UTC
policy_change = datetime(2025, 6, 1, tzinfo=UTC)
snapshot = await ast.history(
"user consent preferences",
bank_id="gdpr-audit",
as_of=policy_change,
tags=["consent"],
)
for hit in snapshot.hits:
print(hit.memory_id, hit.retained_at, hit.text)

Check what changed between two snapshots:

from datetime import datetime, UTC
before = await ast.history("Alice employment", bank_id="contacts", as_of=datetime(2025, 1, 1, tzinfo=UTC))
after = await ast.history("Alice employment", bank_id="contacts", as_of=datetime(2025, 6, 1, tzinfo=UTC))
before_ids = {h.memory_id for h in before.hits}
after_ids = {h.memory_id for h in after.hits}
print("Added: ", after_ids - before_ids)
print("Removed:", before_ids - after_ids)
POST /v1/history
Content-Type: application/json
Authorization: Bearer <token>
{
"query": "What did we know about Alice's employer?",
"bank_id": "user-alice",
"as_of": "2025-01-01T00:00:00Z",
"max_results": 10
}
Terminal window
curl -X POST https://gateway.example.com/v1/history \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{
"query": "What did we know about Alice'\''s employer?",
"bank_id": "user-alice",
"as_of": "2025-01-01T00:00:00Z"
}'

audit() — Gap analysis for a memory bank

Section titled “audit() — Gap analysis for a memory bank”

Scans a memory bank and asks an LLM judge to identify topical gaps — subject areas that are missing, thin, or outdated given the purpose of the bank. Use audit() to surface coverage blind spots before they affect recall quality.

Requires: a Tier 1 pipeline with an LLM provider. Astrocyte.audit() recalls candidate memories, then asks the configured LLM judge to identify gaps.

async def audit(
self,
scope: str,
bank_id: str,
*,
max_memories: int = 50,
max_tokens: int | None = None,
tags: list[str] | None = None,
) -> AuditResult:
ParameterTypeDescription
scopestrNatural-language description of what this bank is supposed to cover. The LLM judge uses this to evaluate coverage. Example: "customer support interactions for ACME Corp".
bank_idstrBank to audit.
max_memoriesintMaximum number of recent memories to feed to the judge. Defaults to 50. Increase for thorough audits; lower for faster / cheaper scans.
max_tokensintOptional token budget. When set, only memories that fit within this budget are passed to the judge.
tagslist[str]Optional tag filter — only memories with at least one of these tags are included in the sample.
FieldTypeDescription
scopestrThe scope string passed to audit().
bank_idstrThe bank that was audited.
coverage_scorefloat0.0–1.0 score from the LLM judge. 1.0 = excellent coverage, 0.0 = severely lacking.
gapslist[GapItem]Identified gap areas, ordered by severity. Empty when coverage is complete.
memories_scannedintNumber of memories the judge actually saw.
traceRecallTrace | NoneDiagnostic trace of the underlying recall used to sample memories.
FieldTypeAllowed values
topicstrShort label for the missing topic area.
severitystr"high", "medium", or "low"
reasonstrOne-sentence explanation of why the gap matters.

Quick coverage check:

from astrocyte import Astrocyte
brain = Astrocyte.from_config("astrocyte.yaml")
result = await brain.audit(
"customer support tickets and resolutions for ACME Corp",
bank_id="support-acme",
)
print(f"Coverage score: {result.coverage_score:.0%}")
for gap in result.gaps:
print(f" [{gap.severity.upper()}] {gap.topic}{gap.reason}")

Scheduled weekly audit with alerting:

import asyncio
from astrocyte import Astrocyte
async def weekly_audit():
brain = Astrocyte.from_config("astrocyte.yaml")
result = await brain.audit(
"internal engineering runbooks and incident postmortems",
bank_id="eng-runbooks",
max_memories=100,
)
high_gaps = [g for g in result.gaps if g.severity == "high"]
if high_gaps:
print(f"⚠️ {len(high_gaps)} high-severity gaps found (score: {result.coverage_score:.0%})")
for g in high_gaps:
print(f" • {g.topic}: {g.reason}")
else:
print(f"✅ Coverage looks good ({result.coverage_score:.0%})")
asyncio.run(weekly_audit())

Tag-scoped audit — only check onboarding content:

result = await brain.audit(
"employee onboarding guides and HR policies",
bank_id="hr-docs",
tags=["onboarding"],
max_memories=30,
)
POST /v1/audit
Content-Type: application/json
Authorization: Bearer <token>
{
"scope": "customer support tickets and resolutions for ACME Corp",
"bank_id": "support-acme",
"max_memories": 50
}

Response:

{
"scope": "customer support tickets and resolutions for ACME Corp",
"bank_id": "support-acme",
"coverage_score": 0.62,
"memories_scanned": 47,
"gaps": [
{
"topic": "Billing disputes",
"severity": "high",
"reason": "No memories found covering refund or chargeback resolution procedures."
},
{
"topic": "Escalation paths",
"severity": "medium",
"reason": "Only one memory references the escalation process; edge cases are absent."
}
]
}
Terminal window
curl -X POST https://gateway.example.com/v1/audit \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{
"scope": "customer support tickets and resolutions for ACME Corp",
"bank_id": "support-acme",
"max_memories": 50
}'

reflect() — Synthesize an answer from memory

Section titled “reflect() — Synthesize an answer from memory”

Recalls relevant memories and synthesizes a natural-language answer. Use reflect when you want an interpreted response rather than raw hits.

async def reflect(
self,
query: str,
bank_id: str | None = None,
*,
banks: list[str] | None = None,
strategy: Literal["cascade", "parallel", "first_match"] | MultiBankStrategy | None = None,
max_tokens: int | None = None,
tags: list[str] | None = None,
context: AstrocyteContext | None = None,
include_sources: bool = True,
dispositions: Any | None = None,
) -> ReflectResult:
ParameterTypeDescription
querystrNatural-language question to answer from memory.
bank_idstrSingle bank to search. Mutually exclusive with banks.
bankslist[str]Multiple banks to search.
strategystr | MultiBankStrategyMulti-bank search strategy.
max_tokensintToken budget for the synthesised answer.
tagslist[str]Filter source memories by tag.
contextAstrocyteContextOptional actor identity and session context.
include_sourcesboolReturn the source memories used. Defaults to True.
dispositionsAnyOptional disposition hints for answer style.
FieldTypeDescription
answerstrSynthesised natural-language answer.
confidencefloat | NoneConfidence score between 0 and 1.
sourceslist[MemoryHit] | NoneSource memories the answer was derived from.
observationslist[str] | NoneAdditional observations from the synthesis.
authority_contextstr | NoneAuthority resolution context, if applicable.
result = await ast.reflect(
"What are this customer's communication preferences?",
bank_id="user-prefs",
include_sources=True,
)
print(result.answer)
# "The customer prefers dark-mode UI and weekly email digests."
print(result.confidence)
# 0.92
for src in result.sources:
print(f" - {src.memory_id}: {src.text[:60]}...")
Terminal window
curl -X POST https://gateway.example.com/v1/reflect \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{
"query": "What are this customer'\''s communication preferences?",
"bank_id": "user-prefs",
"max_tokens": 200,
"include_sources": true
}'

Deletes or archives memories from a bank. Supports targeted deletion by ID, by tag, by date, or full bank wipe.

async def forget(
self,
bank_id: str,
*,
memory_ids: list[str] | None = None,
tags: list[str] | None = None,
scope: str | None = None,
context: AstrocyteContext | None = None,
compliance: bool = False,
reason: str | None = None,
before_date: datetime | None = None,
) -> ForgetResult:
ParameterTypeDescription
bank_idstrBank to delete from.
memory_idslist[str]Specific memory IDs to delete.
tagslist[str]Delete all memories matching these tags.
scopestrSet to "all" to delete every memory in the bank.
contextAstrocyteContextOptional actor identity and session context.
complianceboolMark as a compliance-driven deletion (logged for audit). Defaults to False.
reasonstrFree-text reason for the deletion (stored in audit log).
before_datedatetimeDelete memories with occurred_at before this date.
FieldTypeDescription
deleted_countintNumber of memories permanently deleted.
archived_countintNumber of memories archived instead of deleted.

Delete specific IDs:

result = await ast.forget(
"user-prefs",
memory_ids=["mem_3f8a", "mem_91cb"],
)
print(result.deleted_count) # 2

Delete by tag:

result = await ast.forget("user-prefs", tags=["deprecated"])

Delete all memories in a bank:

result = await ast.forget("user-prefs", scope="all")

Compliance deletion with reason:

result = await ast.forget(
"user-prefs",
memory_ids=["mem_3f8a"],
compliance=True,
reason="GDPR erasure request #4821",
)

Delete before a date:

from datetime import datetime
result = await ast.forget(
"logs",
before_date=datetime(2025, 1, 1),
reason="Retention policy: remove data older than 1 year",
)
Terminal window
curl -X POST https://gateway.example.com/v1/forget \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{
"bank_id": "user-prefs",
"memory_ids": ["mem_3f8a", "mem_91cb"]
}'

Delete by tag:

Terminal window
curl -X POST https://gateway.example.com/v1/forget \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{
"bank_id": "user-prefs",
"tags": ["deprecated"]
}'

compile() — Build wiki pages from memory

Section titled “compile() — Build wiki pages from memory”

Compiles raw memories in a bank into durable, topic-oriented wiki pages. This is optional and requires a configured WikiStore plus Tier 1 pipeline.

async def compile(
self,
bank_id: str,
*,
scope: str | None = None,
) -> CompileResult:
{
"bank_id": "engineering",
"scope": "deployment"
}
Terminal window
curl -X POST https://gateway.example.com/v1/compile \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{"bank_id": "engineering", "scope": "deployment"}'

Mental models are curated, scope-bound summaries persisted as a first-class SPI alongside raw memories, observations, and wiki pages. Hindsight inspired the layer; Astrocyte’s M9 ships them with explicit revision history, scope keys (bank / tag:<name> / entity:<id>), and source-id provenance so revisions trace back to the underlying memories.

Use them when you have a stable “what does this user generally believe / want / prefer about X” answer that the agentic-reflect loop should consult before falling back to raw recall. Open-domain questions (“Would Alice still pursue counseling after the move?”) synthesize faster against a mental-model summary than they do against the raw memory pool.

The Postgres reference adapter (astrocyte-postgres) ships PostgresMentalModelStore registered under the astrocyte.mental_model_stores entry-point group; configure with mental_model_store: postgres in astrocyte.yaml. See provider-spi.md for the SPI shape and mental-models.md for storage detail.

from astrocyte import Astrocyte
brain = Astrocyte.from_config("astrocyte.yaml")
# brain.set_mental_model_store(...) is wired automatically when
# `mental_model_store: postgres` is set in config; you can also pass
# a custom MentalModelStore implementation directly.
Terminal window
curl -X POST https://gateway.example.com/v1/mental-models \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{
"bank_id": "user-alice",
"model_id": "preferences-v1",
"title": "Alice's preferences",
"content": "Alice prefers async communication, dark mode, and Python over Go.",
"scope": "bank",
"source_ids": ["mem-1", "mem-2", "mem-3"]
}'

scope is one of "bank" (default — applies to the whole bank), "tag:<name>", or "entity:<id>". source_ids is an optional list of memory IDs the model summarizes; they are persisted as provenance and used by observation-style invalidation when the underlying memories change.

Terminal window
# List all models in a bank (optionally filtered by scope)
curl -G https://gateway.example.com/v1/mental-models \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
--data-urlencode "bank_id=user-alice" \
--data-urlencode "scope=tag:preferences"
# Get one
curl https://gateway.example.com/v1/mental-models/preferences-v1?bank_id=user-alice \
-H "Authorization: Bearer $ASTROCYTE_TOKEN"
# Refresh — creates a new revision, preserves history
curl -X POST https://gateway.example.com/v1/mental-models/preferences-v1/refresh \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{
"bank_id": "user-alice",
"content": "Updated: Alice now prefers Slack over email.",
"source_ids": ["mem-7", "mem-8"]
}'
# Delete (requires ``forget`` permission, not ``write`` — destructive)
curl -X DELETE https://gateway.example.com/v1/mental-models/preferences-v1?bank_id=user-alice \
-H "Authorization: Bearer $ASTROCYTE_TOKEN"

Mental models share the same scope+invalidation model as observations. When the memories that produced a mental model are updated or forgotten, you can explicitly invalidate dependent observations through:

Terminal window
curl -X POST https://gateway.example.com/v1/observations/invalidate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{"bank_id": "user-alice", "source_ids": ["mem-1", "mem-2"]}'

Like /v1/forget and /v1/mental-models/{id} (delete), this endpoint requires the forget permission rather than write — the operation is destructive of derived rows.


Astrocyte can maintain a knowledge graph of named entities alongside your vector memories. When entity extraction is enabled, retain() automatically identifies entities (people, companies, projects, etc.) and stores them in a graph store. The entity graph powers cross-bank relationship queries and alias-of deduplication (entity resolution).

Requires: a GraphStore backend for graph_search() and graph_neighbors(). The built-in InMemoryGraphStore is available for testing; production deployments use astrocyte-neo4j (Neo4j). Note: section-level graph traversal (entity bridging, causal links) runs over built-in flat SQL tables and does not require a GraphStore adapter.

Represents a named thing in the knowledge graph.

FieldTypeDescription
idstrStable identifier, unique within a bank.
namestrHuman-readable display name.
entity_typestrCategory label, e.g. "PERSON", "ORG", "PROJECT".
metadatadict | NoneArbitrary key-value metadata.

Represents a directed relationship between two entities.

FieldTypeDescription
entity_astrID of the source entity.
entity_bstrID of the target entity.
link_typestrRelationship type, e.g. "alias_of", "co_occurs_with", "reports_to".
evidencestrFree-text passage that supports this link. Defaults to "".
confidencefloat0.0–1.0 confidence score. Defaults to 1.0.
created_atdatetime | NoneWhen the link was created. Set automatically when stored.
metadatadict | NoneArbitrary key-value metadata.

When EntityResolver is wired into the orchestrator, each retain() call runs an alias-of check in the background: new entities are compared against existing candidates and, if an LLM judge confirms they refer to the same real-world entity, an alias_of link is created automatically.

from astrocyte import Astrocyte
# Enable entity resolution by setting graph_store + resolver in astrocyte.yaml:
# graph_store: neo4j
# graph_store_config:
# uri: bolt://localhost:7687
# user: neo4j
# password: ${NEO4J_PASSWORD}
# entity_resolution:
# enabled: true
# similarity_threshold: 0.8 # vector similarity for candidate lookup
# confirmation_threshold: 0.75 # LLM confidence to accept the alias
brain = Astrocyte.from_config("astrocyte.yaml")
# retain() runs extraction + resolution automatically
await brain.retain(
"Alice Smith (formerly Alice Johnson) joined the platform team.",
bank_id="people",
)

For direct graph access use the GraphStore SPI in your own orchestrator. The Astrocyte class also exposes graph convenience methods for common searches.

Find entity candidates by name:

entities = await brain.graph_search("Alice", bank_id="people", limit=5)
for c in entities:
print(c.id, c.name, c.entity_type)

Query neighbors — which memories mention entities related to a given entity?

from astrocyte.types import MemoryEntityAssociation
hits = await brain.graph_neighbors(
entity_ids=["person:alice-smith"],
bank_id="people",
limit=20,
)
for h in hits:
print(h.memory_id, h.text)

Manually store a link:

from astrocyte.types import EntityLink
link = EntityLink(
entity_a="person:alice-smith",
entity_b="person:alice-johnson",
link_type="alias_of",
evidence="Alice Smith (formerly Alice Johnson)",
confidence=0.95,
)
# For custom links, use a configured GraphStore adapter directly.
link_id = await graph_store.store_entity_link(link, bank_id="people")
Terminal window
curl -X POST https://gateway.example.com/v1/graph/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{"query": "Alice", "bank_id": "people", "limit": 5}'
curl -X POST https://gateway.example.com/v1/graph/neighbors \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{"entity_ids": ["person:alice-smith"], "bank_id": "people", "max_depth": 2, "limit": 20}'

export_bank() and import_bank() — Move memories

Section titled “export_bank() and import_bank() — Move memories”

Exports and imports a bank using Astrocyte Memory Archive (AMA) JSONL. These operations are intended for operations, backup, migration, and provider portability. They require admin access when access control is enabled.

async def export_bank(
self,
bank_id: str,
path: str,
*,
include_embeddings: bool = False,
include_entities: bool = True,
allowed_roots: list[str] | None = None,
allow_uncontained: bool = False,
context: AstrocyteContext | None = None,
) -> int:
async def import_bank(
self,
bank_id: str,
path: str,
*,
on_conflict: str = "skip",
allowed_roots: list[str] | None = None,
allow_uncontained: bool = False,
context: AstrocyteContext | None = None,
progress_fn: Any = None,
) -> ImportResult:

allowed_roots and allow_uncontained propagate to the path containment check. By default (no roots configured and allow_uncontained=False), both functions raise ValueError rather than silently writing to arbitrary paths — set ASTROCYTE_PORTABILITY_ROOTS, pass allowed_roots=[<dir>], or set allow_uncontained=True for trusted internal callers. See memory-portability.md §0 for the full security model.

The standalone gateway reads and writes paths on the server filesystem. The gateway always requires ASTROCYTE_PORTABILITY_ROOTS to be configured — when unset, the endpoints return HTTP 422 with the operator hint listing the three opt-in mechanisms.

Terminal window
curl -X POST https://gateway.example.com/v1/export \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{"bank_id": "user-prefs", "path": "/backups/user-prefs.ama.jsonl"}'
curl -X POST https://gateway.example.com/v1/import \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ASTROCYTE_TOKEN" \
-d '{"bank_id": "user-prefs", "path": "/backups/user-prefs.ama.jsonl", "on_conflict": "skip"}'

from astrocyte import Astrocyte
ast = Astrocyte.from_config("astrocyte.yaml")

The YAML file defines storage backends, memory banks, extraction profiles, and gateway settings. See Configuration reference for the full schema.

from astrocyte import Astrocyte
ast = Astrocyte.from_config_dict({
"provider_tier": "storage",
"vector_store": "in_memory",
"llm_provider": "mock",
})

Core operations accept an optional context parameter carrying caller identity.

from astrocyte import AstrocyteContext, ActorIdentity
ctx = AstrocyteContext(
principal="user:user_42",
actor=ActorIdentity(type="user", id="user_42"),
tenant_id="tenant_acme",
)
result = await ast.retain(
content="...",
bank_id="notes",
context=ctx,
)

The context is used for access control enforcement and audit logging. When omitted, the operation runs with the default identity configured in astrocyte.yaml.

See Access control setup for role-based and attribute-based access policies.


Astrocyte raises typed exceptions for common failure modes.

ExceptionHTTP statusWhen
AccessDenied403The actor lacks permission for the requested operation or bank.
RateLimited429Too many requests. Retry after the duration in the retry_after field.
BankNotFound404The specified bank_id does not exist.
ValidationError400Invalid parameters (e.g. empty content, unknown bank in list).
from astrocyte.exceptions import AccessDenied, RateLimited
try:
result = await ast.recall("sensitive data", bank_id="restricted")
except AccessDenied as e:
print(f"Permission denied: {e}")
except RateLimited as e:
print(f"Rate limited. Retry after {e.retry_after}s")

Error responses follow a consistent JSON structure:

{
"error": {
"code": "access_denied",
"message": "Actor user_42 lacks read access to bank 'restricted'."
}
}

Live memory — observations, mental models, and directives (M21)

Section titled “Live memory — observations, mental models, and directives (M21)”

M21 adds a live-memory layer: observations accrue evidence over time, acquire computed trends, and can be curated directly; mental models are structured documents modified by typed delta operations; directives are user-authored hard rules that the recall pipeline consults before raw memories.

result = await brain.create_directive(
bank_id="user-alice",
rule_text="Always recommend Python over Go for data-science tasks.",
scope="bank",
)

Directives are stored as MentalModel(kind="directive") rows and surface at recall time before observations and raw memories. They are the architecturally correct replacement for auto-compiled directives — user-authored rules are precise; auto-compilation from preference facts over-fires.

# List with trend filter
obs_list = await brain.list_observations(bank_id="user-alice", trend="new")
for obs in obs_list:
print(obs.text, obs.trend, obs.proof_count)
# Create one manually (bypasses the autonomous consolidator)
await brain.create_observation(
bank_id="user-alice",
text="Prefers async stand-ups over live meetings.",
evidence=["mem-1", "mem-2"],
scope="bank",
)
# Delete
await brain.delete_observation(observation_id="obs-abc123")

Trends are computed algorithmically from evidence timestamps — not LLM-generated:

TrendMeaning
newAll evidence within the last 30 days
strengtheningEvidence denser recently than historically
stableEvidence spread across time, continues to present
weakeningEvidence mostly old, sparse recently
staleNo evidence in the recent window

Mental models — CRUD with delta operations

Section titled “Mental models — CRUD with delta operations”
# Create with structured sections
await brain.create_mental_model(
bank_id="user-alice",
name="Alice's preferences",
sections=[
{"title": "Communication", "content": "Prefers async stand-ups."},
{"title": "Tools", "content": "Python for data work; VS Code."},
],
)
# Update with typed delta operations (only changed blocks are re-emitted)
result = await brain.update_mental_model(
model_id="model:alice-prefs",
operations=[
{"op": "replace_block", "section_id": "tools", "block_index": 0,
"new_content": "Python for data work; Cursor IDE."},
],
)
print(result.applied_ops, result.skipped_ops)
# List
models = await brain.list_mental_models(bank_id="user-alice", scope="bank")
# Delete (soft)
await brain.delete_mental_model(model_id="model:alice-prefs")

Delta operations modify only the targeted sections and blocks; untouched content is physically copied through — LLM drift on unchanged content is structurally impossible.

MCP tools (for agents via MCP server): memory_create_directive, memory_list_observations, memory_get_observation, memory_create_observation, memory_delete_observation, memory_list_mental_models, memory_create_mental_model, memory_update_mental_model, memory_delete_mental_model.

See Observation evolution for the full guide.