Memory

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>;
}
FieldTypePurpose
idstringUnique identifier for the layer
namestringHuman-readable display name
slotnumberOrdering priority -- lower slots appear first in the assembled view
scopeMemoryScopePersistence boundary for stored state
budgetBudgetConfigToken budget allocation for this layer
hooksMemoryHooks<TState>Lifecycle callbacks
timeoutsPartial<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:

ScopeMeaning
'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 decide

When 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;
}
FieldTypePurpose
tokenBudgetnumberTotal token budget for all memory layers combined
responseReservenumberTokens reserved for the model's response
overflow'truncate' | 'summarize' | 'sliding_window'Strategy when total recall exceeds the budget
overflowModelstringModel used for summarization overflow (when overflow is 'summarize')
windowSizenumberNumber 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

HookWhenPurpose
initAgent startsLoad persisted state from storage
recallBefore each LLM callInject items into the prompt
storeAfter each LLM responseExtract and persist new knowledge
onSpawnChild agent spawnedDecide what state the child inherits
onReturnChild agent completesMerge child results back into parent state
onCompleteAgent finishesFinal persistence with outcome metadata
disposeCleanupRelease 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 slot

LayerTimeouts

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

On this page