---
title: "Observational Memory"
description: "Background three-tier compaction (recent raw → observations → reflections) that keeps long agent threads cheap and prompt-cache-stable without touching short conversations."
---

# Observational Memory

A long-running agent thread accumulates a huge transcript: every message, every tool call, every result. Replaying that whole history into the model on each turn is expensive and eventually blows the context window. **Observational Memory (OM)** compacts the older part of a long thread into a dated, layered summary so the model still knows what happened — just at a fraction of the token cost — while the most recent turns stay verbatim.

OM is entirely automatic and owner-scoped. **Short threads are unaffected**: until a thread crosses the first compaction threshold, OM is a no-op and the context is byte-for-byte what it would be without it.

## The three tiers {#tiers}

OM represents a long thread as three layers, from most-distilled to most-recent:

| Tier                    | What it is                                                                                        |
| ----------------------- | ------------------------------------------------------------------------------------------------- |
| **Reflections**         | Highest-level, condensed from the observation log once it grows large. The long-arc summary.      |
| **Observations**        | Dense, dated entries that fold a stretch of raw messages into a compact record of what happened.  |
| **Recent raw messages** | The last N turns, kept **verbatim** — never folded — so the agent always sees the latest context. |

<Diagram id="doc-block-2hncj4" title="Three tiers, distilled to recent" summary={"The older prefix folds into dated observations and a long-arc reflection; only the most recent turns stay verbatim."}>

```html
<div class="om">
  <div class="diagram-card">
    <span class="diagram-pill">Reflections</span
    ><small class="diagram-muted"
      >long-arc summary, condensed from the observation log</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&uarr;</div>
  <div class="diagram-card">
    <span class="diagram-pill accent">Observations</span
    ><small class="diagram-muted"
      >dense, dated entries folding stretches of raw messages</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&uarr;</div>
  <div class="diagram-card">
    <span class="diagram-pill ok">Recent raw messages</span
    ><small class="diagram-muted"
      >last N turns, kept <strong>verbatim</strong> — never folded</small
    >
  </div>
</div>
```

```css
.om {
  display: flex;
  flex-direction: column-reverse;
  align-items: stretch;
  gap: 8px;
}
.om .diagram-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 16px;
}
.om .diagram-arrow {
  text-align: center;
  font-size: 20px;
  line-height: 1;
}
```

</Diagram>

On each turn, the read side assembles these into a single self-labeled `[Observational Memory]` block that replaces the raw older prefix, keeps the recent-raw window intact, and tells the model to treat the compacted record as authoritative (don't redo completed work, trust the recorded decisions, names, dates, and status).

## How compaction runs {#compaction}

Two passes run as a **fire-and-forget, best-effort** step _after_ a clean turn, so they never add latency to the user-visible response and any failure is swallowed:

1. **Observer** — once a thread's _unobserved_ messages exceed the observation token threshold, folds them into a single dense observation entry.
2. **Reflector** — once the persisted observation log itself exceeds the reflection token threshold, condenses the observations into a higher-level reflection.

<Diagram id="doc-block-1rds4e4" title="Two best-effort passes after a clean turn" summary="Each pass no-ops below its threshold, so running the compactor every turn is cheap. Failures are swallowed and never add latency.">

```html
<div class="om-pass">
  <div class="diagram-node">
    Clean turn ends<br /><small class="diagram-muted">fire-and-forget</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-card">
    <span class="diagram-pill accent">Observer</span
    ><small class="diagram-muted"
      >unobserved tokens &gt; 30k? &rarr; fold into one observation</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-card">
    <span class="diagram-pill accent">Reflector</span
    ><small class="diagram-muted"
      >observation log &gt; 40k? &rarr; condense into a reflection</small
    >
  </div>
</div>
```

```css
.om-pass {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.om-pass .diagram-node,
.om-pass .diagram-card {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 14px;
}
.om-pass .diagram-arrow {
  font-size: 22px;
  line-height: 1;
}
```

</Diagram>

Both passes no-op below their thresholds, so calling the compactor after every turn is cheap. Because OM replaces the volatile raw prefix with stable compacted text, it also keeps the prompt **cache-stable** across turns of a long thread.

OM data lives in the app's own SQL database, scoped to the owner (and org when present) — the same scoping model as the rest of the framework. It is never shared across users.

## Configuration {#config}

Defaults are conservative. An operator can dial compaction at deploy time with `AGENT_NATIVE_OM_*` environment variables (no redeploy of the app code needed); an invalid or missing value always falls back to the named default.

| Env var                                       | Default | What it controls                                                                       |
| --------------------------------------------- | ------- | -------------------------------------------------------------------------------------- |
| `AGENT_NATIVE_OM_OBSERVATION_TOKEN_THRESHOLD` | `30000` | Unobserved-message tokens that trigger the Observer to fold them into one observation. |
| `AGENT_NATIVE_OM_REFLECTION_TOKEN_THRESHOLD`  | `40000` | Observation-log tokens that trigger the Reflector to condense into a reflection.       |
| `AGENT_NATIVE_OM_RECENT_RAW_MESSAGE_COUNT`    | `12`    | How many of the most-recent messages stay verbatim (never folded into an observation). |

The Observer and Reflector output caps (4000 / 2000 tokens) keep a single compaction pass from itself blowing the budget; they are tunable in code via `resolveObservationalMemoryConfig({ ... })` but not env-exposed.

<Callout tone="info">
  Lower the thresholds to compact sooner (cheaper long threads, slightly more
  summarization); raise them to keep more raw history in context before
  compacting. Set `AGENT_NATIVE_OM_RECENT_RAW_MESSAGE_COUNT` higher if your
  workflows need a longer verbatim tail.
</Callout>

## When it kicks in {#when}

OM only changes behavior for threads long enough to have produced at least one observation or reflection. Concretely:

- A brand-new or short thread: no OM entries yet → the context is the plain transcript, unchanged.
- A long thread that has crossed the observation threshold: the older prefix is replaced by the compacted `[Observational Memory]` block, the recent-raw tail stays verbatim, and token usage drops substantially.

The injection is best-effort and boundary-safe — if a safe trim point can't be found (e.g. a pending tool-use/result pair sits at the window edge), OM injects the memory block _additively_ without trimming rather than risk dropping a pending tool result.

## What's next

- [**Using Your Agent**](/docs/using-your-agent) — the day-to-day loop of working with the agent docked next to your app.
- [**Observability**](/docs/observability) — token and cost metrics per run, where OM's savings show up.
- [**Custom Agents & Teams**](/docs/agent-teams) — long sub-agent runs benefit from the same compaction.
