diff --git a/web/astro.config.mjs b/web/astro.config.mjs
index b02bbc88d..1d29dd6e3 100644
--- a/web/astro.config.mjs
+++ b/web/astro.config.mjs
@@ -10,8 +10,9 @@ export default defineConfig({
trailingSlash: "always",
integrations: [
mermaid({
- theme: 'forest',
- autoTheme: true
+ theme: "neutral",
+ autoTheme: true,
+ enableLog: false,
}),
starlight({
title: "Prompty",
diff --git a/web/src/content/docs/agentic-concepts/agent-loop.mdx b/web/src/content/docs/agentic-concepts/agent-loop.mdx
index 69970100f..c572ec906 100644
--- a/web/src/content/docs/agentic-concepts/agent-loop.mdx
+++ b/web/src/content/docs/agentic-concepts/agent-loop.mdx
@@ -178,7 +178,11 @@ The loop does not invent tool implementations. A `.prompty` file can declare
tool schemas, but your application still supplies the functions or provider
implementations that execute them.
-The loop also does not automatically persist memory. Conversation history is
-passed by the application, usually through a `kind: thread` input. See
-[Conversation History](/core-concepts/conversation-history/) for the thread
-pattern.
+The loop also does not automatically choose a memory backend. Prompty defines
+the portable memory store and deterministic recall/formatting behavior, but
+your application still supplies the persistence hook and lifecycle policy. See
+[Memory](/agentic-concepts/memory/) for the host/runtime boundary.
+
+Conversation history is passed by the application, usually through a
+`kind: thread` input. See [Conversation History](/core-concepts/conversation-history/)
+for the thread pattern.
diff --git a/web/src/content/docs/agentic-concepts/index.mdx b/web/src/content/docs/agentic-concepts/index.mdx
index 86b1f05c2..fe8f42f3c 100644
--- a/web/src/content/docs/agentic-concepts/index.mdx
+++ b/web/src/content/docs/agentic-concepts/index.mdx
@@ -2,7 +2,8 @@
title: Agentic Concepts
description: >
Understand Prompty's agentic runtime model: turns, tool-calling loops,
- guardrails, context compaction, steering, cancellation, events, and retries.
+ guardrails, memory, context compaction, steering, cancellation, events, and
+ retries.
sidebar:
order: 1
---
@@ -16,6 +17,7 @@ tool-using agents. After reading it, you should be able to answer:
- where the boundary is between a `.prompty` file and host runtime code;
- how a single `turn()` differs from a full chat session;
+- how memory is represented by Prompty and persisted by your host;
- when messages are prepared, appended, trimmed, compacted, or steered;
- where to enforce policy with guardrails instead of prompt instructions;
- how to observe and stop a long-running tool loop.
@@ -38,6 +40,7 @@ flowchart TD
C -. "optional controls" .-> K["context budget + compaction"]
C -. "optional controls" .-> L["guardrails"]
C -. "optional controls" .-> M["steering"]
+ C -. "host-provided context" .-> N["memory store"]
```
The key distinction is:
@@ -46,6 +49,9 @@ The key distinction is:
- **`turn()`** runs one user turn and, if needed, loops internally for tool calls.
- **Tool-loop iterations do not re-render the template.** They mutate the prepared
message array by appending assistant/tool messages.
+- **Memory is a host boundary.** Prompty defines the portable memory model and
+ deterministic behavior; your application decides where the memory store lives
+ and when it is loaded or saved.
- **External user turns call `turn()` again.** If you pass conversation history as
a thread input, that thread is expanded during the next `prepare()` call.
@@ -54,6 +60,7 @@ The key distinction is:
| Concept | What it controls | Start here |
|---|---|---|
| Agent loop | How tool calls are executed and fed back to the model | [Agent Loop](/agentic-concepts/agent-loop/) |
+| Memory | Portable memory semantics, recall, formatting, and host persistence boundaries | [Memory](/agentic-concepts/memory/) |
| Runtime controls | The per-iteration order for cancellation, steering, context, guardrails, model calls, and tools | [Runtime Controls](/agentic-concepts/runtime-controls/) |
| Guardrails | Validate or rewrite inputs, outputs, and tool arguments | [Guardrails](/agentic-concepts/guardrails/) |
| Context budget | Trim messages before model calls | [Context & Compaction](/agentic-concepts/context-compaction/) |
@@ -76,10 +83,16 @@ available in the v2 runtimes:
| C# | Yes | Yes | Yes | Yes |
| Rust | Yes | Yes | Yes | Yes |
+Memory helpers and conformance vectors are available across the generated model
+and runtime surface. The host still owns persistence by implementing the runtime's
+memory port or equivalent load/save adapter.
+
## Related docs
- [Agent tool calling guide](/how-to/agent-tool-calling/) for a hands-on walkthrough
- [Tools](/core-concepts/tools/) for `.prompty` tool declarations
- [Conversation history](/core-concepts/conversation-history/) for thread inputs
+- [Memory reference](/reference/memorystore/) for the generated `MemoryStore`
+ contract
- [Tracing](/core-concepts/tracing/) for observing runtime execution
- [Agent loop specification](/specification/agent-loop/) for the lower-level contract
diff --git a/web/src/content/docs/agentic-concepts/memory.mdx b/web/src/content/docs/agentic-concepts/memory.mdx
new file mode 100644
index 000000000..1c13271dc
--- /dev/null
+++ b/web/src/content/docs/agentic-concepts/memory.mdx
@@ -0,0 +1,230 @@
+---
+title: Memory
+description: >
+ How Prompty represents memory, what behavior the runtimes provide, and where
+ your host application plugs in persistence.
+sidebar:
+ order: 6
+---
+
+import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
+
+Prompty memory is a **portable runtime contract**, not a hosted database. The
+runtimes know how to work with memory once a store is available; your
+application decides where that store lives and when to load or save it.
+
+That split keeps memory behavior consistent across runtimes without forcing
+every app into the same storage, privacy, or lifecycle model.
+
+## Mental model
+
+```mermaid
+flowchart LR
+ A["Host application"] --> B["load MemoryStore"]
+ B --> C["Prompty turn()"]
+ C --> D["core memories injected into system prompt"]
+ C --> E["recall / remember / update / clear"]
+ E --> F["updated MemoryStore"]
+ F --> G["host saves store"]
+
+ H["Storage choice\nfile, database, service"] -. owned by host .-> A
+```
+
+Prompty owns the portable behavior:
+
+- `MemoryStore`, `MemoryEntry`, and `MemoryCategory`;
+- deterministic `recall` scoring and formatting;
+- `remember`, `update`, `remove`, `clear`, and cap-based eviction;
+- core-memory formatting for model-visible system context;
+- conformance vectors that keep those semantics aligned across runtimes.
+
+Your host owns the integration:
+
+- persistence location: file, database, profile service, project store, or
+ tenant-scoped service;
+- scoping: user, project, agent, session, organization, or some combination;
+- lifecycle policy: when to remember, summarize, update, expire, or ask for
+ permission;
+- privacy and security boundaries: consent, audit, tenant isolation, encryption,
+ and deletion;
+- wiring the store into the turn loop through the runtime's memory port or
+ equivalent load/save adapter.
+
+
+
+## Categories
+
+Memory entries are tiered so runtimes agree on what each entry means.
+
+| Category | Meaning | Runtime behavior |
+|---|---|---|
+| `core` | Persistent facts or preferences that should be model-visible by default | Injected into the system prompt, boosted during recall, and deduplicated by tag set on write |
+| `archival` | Summaries or older context that should be available through recall | Not injected by default; preferred for eviction when the store exceeds its cap |
+| `insight` | Saved reflections or derived observations | Available through recall; not injected by default |
+
+Only `core` memories are formatted directly into the model-visible system prompt:
+
+```text
+## Memory
+- User prefers concise answers
+- Deploys on Friday afternoons
+```
+
+Archival and insight memories stay out of the prompt unless your app recalls
+them and chooses to include the recall results.
+
+## Runtime shape
+
+
+
+
+```python
+from prompty import MemoryEntry, MemoryStore, format_for_system_prompt, recall, remember
+
+store = MemoryStore.load({"entries": []})
+remember(
+ store,
+ MemoryEntry.load(
+ {
+ "content": "User prefers concise answers",
+ "category": "core",
+ "tags": ["preference", "tone"],
+ }
+ ),
+)
+
+system_context = format_for_system_prompt(store)
+results = recall(store, "tone", limit=3)
+```
+
+
+
+
+```ts
+import {
+ MemoryEntry,
+ MemoryStore,
+ formatForSystemPrompt,
+ recall,
+ remember,
+} from "@prompty/core";
+
+const store = MemoryStore.load({ entries: [] });
+remember(
+ store,
+ MemoryEntry.load({
+ content: "User prefers concise answers",
+ category: "core",
+ tags: ["preference", "tone"],
+ }),
+);
+
+const systemContext = formatForSystemPrompt(store);
+const results = recall(store, "tone", 3);
+```
+
+
+
+
+```csharp
+using Prompty.Core;
+
+var store = MemoryStore.Load(new Dictionary { ["entries"] = new List
+
+
+```rust
+use prompty::{MemoryCategory, MemoryEntry, MemoryStore};
+
+let mut store = MemoryStore { entries: vec![] };
+store.remember(
+ MemoryEntry {
+ content: "User prefers concise answers".to_string(),
+ category: MemoryCategory::Core,
+ created_at: None,
+ tags: Some(vec!["preference".to_string(), "tone".to_string()]),
+ },
+ 0,
+);
+
+let system_context = store.format_for_system_prompt();
+let results = store.recall("tone", 3);
+```
+
+
+
+
+## Persistence boundary
+
+Each flagship runtime exposes a memory port shape: load a whole `MemoryStore`
+snapshot, let Prompty apply deterministic behavior, then save the updated
+snapshot. A host can back that port with any storage system.
+
+```mermaid
+sequenceDiagram
+ participant App
+ participant MemoryPort
+ participant Prompty
+ participant Model
+
+ App->>MemoryPort: load scoped store
+ MemoryPort-->>App: MemoryStore
+ App->>Prompty: turn(agent, inputs, memory)
+ Prompty->>Prompty: format core memories
+ Prompty->>Model: messages + memory context
+ Model-->>Prompty: response / tool calls
+ Prompty-->>App: result + updated store
+ App->>MemoryPort: save updated store
+```
+
+Prompty deliberately does not decide when a model response should become a
+memory. Many apps should ask for user permission, apply policy, or summarize
+conversation history before writing. Those choices depend on the host's product
+requirements, not the `.prompty` file.
+
+## Conformance
+
+Memory is covered by shared vectors, so flagship runtimes benefit equally from
+the same semantics. The vectors assert:
+
+- recall ranking by weighted lexical score;
+- tag matches and core-memory boosts;
+- empty-query behavior and result limits;
+- stable tie ordering;
+- core deduplication by tags;
+- archival-first eviction;
+- clear, update, and out-of-range behavior;
+- system-prompt formatting;
+- snapshot round-trip behavior;
+- model-visible memory context in a turn.
+
+If a runtime changes any of those behaviors, its generated conformance suite
+goes red.
+
+## Related docs
+
+- [Agent Loop](/agentic-concepts/agent-loop/) for where memory context appears
+ in a turn
+- [Context & Compaction](/agentic-concepts/context-compaction/) for trimming and
+ summarizing conversation history
+- [MemoryStore reference](/reference/memorystore/) for the generated store shape
+- [MemoryEntry reference](/reference/memoryentry/) for entry fields and category
+ metadata
diff --git a/web/src/styles/custom.css b/web/src/styles/custom.css
index c91449b9c..4c1008ced 100644
--- a/web/src/styles/custom.css
+++ b/web/src/styles/custom.css
@@ -57,3 +57,60 @@
starlight-tabs .tablist-wrapper {
overflow-y: hidden;
}
+
+/* Improve Mermaid contrast in Starlight dark mode. */
+:root[data-theme="dark"] .mermaid {
+ background: var(--sl-color-bg-nav);
+ border: 1px solid var(--sl-color-gray-5);
+ border-radius: 0.5rem;
+ padding: 1rem;
+}
+
+:root[data-theme="dark"] .mermaid svg {
+ color: var(--sl-color-white);
+}
+
+:root[data-theme="dark"] .mermaid .node rect,
+:root[data-theme="dark"] .mermaid .node circle,
+:root[data-theme="dark"] .mermaid .node ellipse,
+:root[data-theme="dark"] .mermaid .node polygon,
+:root[data-theme="dark"] .mermaid .node path,
+:root[data-theme="dark"] .mermaid rect.actor,
+:root[data-theme="dark"] .mermaid .actor.actor-top,
+:root[data-theme="dark"] .mermaid .actor.actor-bottom {
+ fill: var(--sl-color-gray-6) !important;
+ stroke: var(--prompty-blue) !important;
+}
+
+:root[data-theme="dark"] .mermaid .edgeLabel,
+:root[data-theme="dark"] .mermaid .labelBkg,
+:root[data-theme="dark"] .mermaid .labelBox {
+ background-color: var(--sl-color-bg-nav) !important;
+ fill: var(--sl-color-bg-nav) !important;
+}
+
+:root[data-theme="dark"] .mermaid text,
+:root[data-theme="dark"] .mermaid tspan,
+:root[data-theme="dark"] .mermaid .label,
+:root[data-theme="dark"] .mermaid .nodeLabel,
+:root[data-theme="dark"] .mermaid .edgeLabel,
+:root[data-theme="dark"] .mermaid .messageText,
+:root[data-theme="dark"] .mermaid .actor,
+:root[data-theme="dark"] .mermaid .actor tspan,
+:root[data-theme="dark"] .mermaid .label tspan,
+:root[data-theme="dark"] .mermaid .nodeLabel tspan,
+:root[data-theme="dark"] .mermaid .edgeLabel tspan {
+ color: var(--sl-color-white) !important;
+ fill: var(--sl-color-white) !important;
+}
+
+:root[data-theme="dark"] .mermaid .flowchart-link,
+:root[data-theme="dark"] .mermaid .messageLine0,
+:root[data-theme="dark"] .mermaid .messageLine1 {
+ stroke: var(--prompty-blue) !important;
+}
+
+:root[data-theme="dark"] .mermaid marker path {
+ fill: var(--prompty-blue) !important;
+ stroke: var(--prompty-blue) !important;
+}