Memory Layer System
How Noetic's memory layers inject long-term and short-term context into every LLM call through a slot-based, scoped architecture.
Overview
Noetic's memory system is built around MemoryLayer objects. Each layer occupies a numbered slot, declares a scope, and implements lifecycle hooks that run before and after every LLM call. The runtime merges all layer outputs into a single prompt view via assembleView().
MemoryLayer Interface
Every memory layer implements this interface:
interface MemoryLayer<TState = unknown> {
id: string;
name: string;
slot: number;
scope: MemoryScope;
budget?: BudgetConfig;
hooks: MemoryHooks<TState>;
timeouts?: Partial<LayerTimeouts>;
}| Field | Type | Purpose |
|---|---|---|
id | string | Unique identifier for the layer |
name | string | Human-readable display name |
slot | number | Ordering priority -- lower slots appear first in the assembled view |
scope | MemoryScope | Persistence boundary for stored state |
budget | BudgetConfig | Token budget allocation for this layer |
hooks | MemoryHooks<TState> | Lifecycle callbacks |
timeouts | Partial<LayerTimeouts> | Per-hook timeout overrides in milliseconds |
Slot Constants
Slots determine the order in which layer outputs are injected into the prompt. Lower numbers appear first.
const Slot = {
WORKING_MEMORY: 100,
ENTITY: 150,
OBSERVATIONS: 200,
PROCEDURAL: 250,
EPISODIC: 300,
RAG: 350,
SEMANTIC_RECALL: 400,
} as const;You can use any number for custom layers. The built-in constants are guidelines, not hard constraints.
MemoryScope
Scope controls the persistence boundary of a layer's state:
| Scope | Meaning |
|---|---|
'thread' | State is isolated per conversation thread |
'resource' | State is shared across threads tied to the same resource (e.g., a project or document) |
'global' | State is shared across all executions |
'execution' | State lives only for the duration of a single agent run |
type MemoryScope = 'thread' | 'resource' | 'global' | 'execution';BudgetConfig
Controls how many tokens a layer can consume when injecting items into the prompt:
type BudgetConfig =
| number // fixed token count
| { min: number; max: number } // range -- allocator distributes spare tokens
| 'auto'; // let the runtime decideWhen using a { min, max } range, the budget allocator guarantees at least min tokens and distributes remaining capacity up to max.
ProjectionPolicy
The projection policy governs how the runtime assembles all layer outputs into the final prompt:
interface ProjectionPolicy {
tokenBudget: number;
responseReserve: number;
overflow: 'truncate' | 'summarize' | 'sliding_window';
overflowModel?: string;
windowSize?: number;
}| Field | Type | Purpose |
|---|---|---|
tokenBudget | number | Total token budget for all memory layers combined |
responseReserve | number | Tokens reserved for the model's response |
overflow | 'truncate' | 'summarize' | 'sliding_window' | Strategy when total recall exceeds the budget |
overflowModel | string | Model used for summarization overflow (when overflow is 'summarize') |
windowSize | number | Number of recent items to keep (when overflow is 'sliding_window') |
Hook Lifecycle
Memory hooks fire in a deterministic order during agent execution:
init → recall → [LLM call] → store → ... (loop) → onComplete → dispose
↓
onSpawn (child)
↓
onReturn (parent)Hook Summary
| Hook | When | Purpose |
|---|---|---|
init | Agent starts | Load persisted state from storage |
recall | Before each LLM call | Inject items into the prompt |
store | After each LLM response | Extract and persist new knowledge |
onSpawn | Child agent spawned | Decide what state the child inherits |
onReturn | Child agent completes | Merge child results back into parent state |
onComplete | Agent finishes | Final persistence with outcome metadata |
dispose | Cleanup | Release resources |
Hook Type Signatures
interface MemoryHooks<TState = unknown> {
init?: (params: InitParams) => Promise<InitResult<TState>>;
recall?: (params: RecallParams<TState>) => Promise<RecallResult<TState> | null>;
store?: (params: StoreParams<TState>) => Promise<StoreResult<TState> | undefined>;
onSpawn?: (params: SpawnParams<TState>) => Promise<SpawnResult<TState> | null>;
onReturn?: (params: ReturnParams<TState>) => Promise<ReturnResult<TState> | undefined>;
onComplete?: (params: CompleteParams<TState>) => Promise<void | { state: TState }>;
dispose?: (params: DisposeParams<TState>) => Promise<void>;
}assembleView()
The assembleView() function merges all layer recall outputs into a single list of Item objects, ordered by slot number. This list is prepended to the conversation history before the LLM call.
const items = await runtime.assembleView(agentConfig, userInput, ctx);
// items contains memory items from all layers, sorted by slotLayerTimeouts
Override the default timeout (in milliseconds) for any individual hook:
interface LayerTimeouts {
init?: number;
recall?: number;
store?: number;
onSpawn?: number;
onReturn?: number;
onComplete?: number;
dispose?: number;
}If a hook exceeds its timeout, the runtime logs a warning and continues without that layer's contribution.
Next Steps
- Working Memory -- scratchpad for the current turn
- Observational Memory -- auto-extracted facts
- Custom Layers -- build your own