NOETIC
Framework
Memory

Working Memory

A scratchpad memory layer that persists structured or free-form notes across turns within a single execution.

Overview

Working Memory is the simplest built-in memory layer. It acts as a scratchpad that the agent can read and write each turn. The LLM sees the current state as a <working_memory> block in the prompt, and updates it by calling an updateWorkingMemory tool.

  • Slot: 100 (Slot.WORKING_MEMORY)
  • Default scope: thread
  • Default budget: { min: 200, max: 1500 }

Usage

import { workingMemory } from '@noetic-tools/core';

const layer = workingMemory({
  scope: 'thread',
  readOnly: false,
});

Register the layer with a react pattern (or spawn's memory option):

const agent = react({
  model: 'openai/gpt-4o',
  instructions: 'You are a helpful assistant.',
  memory: [layer],
});

Configuration

interface WorkingMemoryConfig {
  scope?: 'thread' | 'resource';
  schema?: ZodType;
  template?: string;
  readOnly?: boolean;
}
FieldTypeDefaultPurpose
scope'thread' | 'resource''thread'Persistence boundary
schemaZodType--Optional Zod schema for structured state (state becomes Record<string, unknown>). Updates are validated against it: the merged state must pass, or the update is rejected
templatestring--Initial template content
readOnlybooleanfalseWhen true, the store hook skips updates

State Type

type WorkingMemoryState = string | Record<string, unknown>;

When no schema is provided, state is a plain string. When a schema is provided, state is an object and updates are deep-merged: object-valued keys merge recursively, while arrays and primitives replace. Prototype-pollution keys (__proto__, constructor) are stripped at every depth.

How It Works

init

Loads saved state from ScopedStorage. Falls back to an empty string (no schema) or empty object (with schema). Persisted state that fails the configured schema is discarded and replaced with {} rather than aborting the execution.

recall

If state is non-empty, wraps it in a <working_memory> XML block and injects it as a developer message item.

store

Watches for updateWorkingMemory function calls in the LLM response. When found, parses the JSON arguments and deep-merges them into the current state. Prototype keys (__proto__, constructor) are stripped for safety.

When a schema is configured, the merged state is validated against it (so partial updates remain legal). A schema-violating update is dropped with a diagnostic on the legacy store path, and throws as a tool error the model can see on the working-memory/update tool path — either way the prior state is untouched.

If readOnly is true, the store hook is a no-op.

onSpawn

When scope is 'resource', the parent state is deep-cloned to the child. For 'thread' scope, children do not inherit working memory.

Example: Structured Scratchpad

import { z } from 'zod';
import { workingMemory } from '@noetic-tools/core';

const layer = workingMemory({
  schema: z.object({
    currentGoal: z.string(),
    completedSteps: z.array(z.string()),
    blockers: z.array(z.string()),
  }),
});

The LLM can then call updateWorkingMemory({ currentGoal: 'Deploy v2', completedSteps: ['write tests'] }) to update specific fields without overwriting the rest.

Provides

Working Memory exposes two entries via the provides API, making them accessible on ctx.memory['working-memory']:

snapshot (data)

A read-only projection of the current working memory state.

const current = ctx.memory['working-memory'].snapshot;
// Returns WorkingMemoryState (string | Record<string, unknown>)

update (function)

Merges key-value pairs into the current state. Accepts a Record<string, unknown> argument.

await ctx.memory['working-memory'].update({ currentGoal: 'Ship v2' });

Because update is a layerFn, the runtime automatically injects it as an LLM tool named working-memory/update. The model can call this tool directly to modify working memory during a conversation turn.

Backward Compatibility

The store hook also detects the legacy updateWorkingMemory function-call convention. If the model emits an updateWorkingMemory call (rather than working-memory/update), the layer handles it identically -- deep-merging the arguments into state.

On this page