Skip to content

Repository files navigation

agent-scratchpad

Lightweight key-value scratchpad for AI agent working memory.

npm versionnpm downloadslicensenodeTypeScript

agent-scratchpad is a zero-dependency, in-process key-value store purpose-built for AI agent reasoning loops. Agents executing multi-step workflows (ReAct, Plan-and-Execute, Chain-of-Thought with tool use) need a place to write down intermediate state between steps -- tool outputs, extracted entities, partial computations, decision rationale, and task decomposition state. This package provides that working memory with typed entries, automatic TTL-based expiration, hierarchical namespaces, tag-based querying, point-in-time snapshots, event-driven change observation, pluggable persistence, and a toContext() method that renders scratchpad contents directly into LLM prompts. It works with any agent framework or custom agent loop.

Installation

npm install agent-scratchpad

Quick Start

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();// Store intermediate resultspad.set('step','analyze');pad.set('user',{id: 42,name: 'Alice'});console.log(pad.get('step'));// 'analyze'console.log(pad.has('user'));// trueconsole.log(pad.keys());// ['step', 'user']// Render contents for an LLM promptconstcontext=pad.toContext({format: 'markdown'});

Features

  • Zero runtime dependencies -- all logic uses built-in JavaScript APIs
  • TypeScript-first -- full generic type safety on get<T>() and set<T>()
  • TTL expiration -- fixed or sliding time-to-live with lazy and active sweep modes
  • Hierarchical namespaces -- scope entries per agent, task, or step with pad.namespace('name')
  • Tag-based querying -- label entries and retrieve them with findByTag()
  • Snapshots -- capture and restore full scratchpad state for backtracking and debugging
  • Context rendering -- format entries as Markdown, XML, JSON, or key-value pairs for LLM prompts
  • Event system -- observe set, delete, expire, and clear events
  • Pluggable persistence -- save and load scratchpad state via a simple adapter interface
  • Framework-agnostic -- works with LangChain, Vercel AI SDK, AutoGen, CrewAI, or any custom loop

API Reference

createScratchpad(options?)

Creates a new Scratchpad instance.

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 60_000,defaultSlidingTtl: false,sweepIntervalMs: 10_000,now: ()=>Date.now(),persistence: adapter,});

Parameters:

OptionTypeDefaultDescription
defaultTtlnumber | nullnullDefault TTL in milliseconds applied to entries that do not specify their own. null means no expiration.
defaultSlidingTtlbooleanfalseWhether the default TTL mode is sliding (resets on access) or fixed (from creation).
sweepIntervalMsnumber | nullnullInterval in milliseconds for proactive background sweep of expired entries. null disables active sweep.
now() => numberDate.nowCustom time source. Useful for deterministic testing.
persistencePersistenceAdapterundefinedOptional adapter for saving and loading scratchpad state.

Returns:Scratchpad


fromSnapshot(snapshot, options?)

Creates a new Scratchpad pre-populated from a previously captured snapshot.

import{fromSnapshot}from'agent-scratchpad';constpad=fromSnapshot(snap,{defaultTtl: 30_000});

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot object previously obtained from pad.snapshot() or pad.serialize().
  • options (ScratchpadOptions, optional) -- Configuration options passed to the underlying createScratchpad() call.

Returns:Scratchpad


Scratchpad Methods

set<T>(key, value, options?)

Stores a value under the given key. If the key already exists, updates the value and updatedAt timestamp while preserving createdAt.

pad.set('result',{score: 0.95});pad.set('cache','value',{ttl: 5_000,tags: ['temporary']});pad.set('session',token,{ttl: 30_000,slidingTtl: true});

Parameters:

  • key (string) -- The entry key.
  • value (T) -- The value to store.
  • options (EntryOptions, optional) -- Per-entry configuration.
OptionTypeDefaultDescription
ttlnumber | nullInherits defaultTtlTTL in milliseconds. null disables expiration for this entry.
slidingTtlbooleanInherits defaultSlidingTtlWhether TTL resets on each get() access.
tagsstring[][]String labels for categorizing the entry.

Returns:void

get<T>(key)

Retrieves the value for a key. If the entry has expired, it is removed, an expire event fires, and undefined is returned. On a successful read, accessedAt is updated (which resets the sliding TTL window if applicable).

constuser=pad.get<{id: number;name: string}>('user');

Parameters:

  • key (string) -- The entry key.

Returns:T | undefined

has(key)

Checks whether a key exists and is not expired. Expired entries are removed and trigger an expire event.

if(pad.has('apiResponse')){// entry is live}

Parameters:

  • key (string) -- The entry key.

Returns:boolean

delete(key)

Removes an entry by key. Fires a delete event if the entry existed.

constremoved=pad.delete('staleData');// true if it existed

Parameters:

  • key (string) -- The entry key.

Returns:boolean -- true if the entry existed and was removed, false otherwise.

clear()

Removes all entries from the scratchpad. Fires a clear event with the count of removed entries.

pad.clear();

Returns:void

keys()

Returns an array of all non-expired keys. Expired entries encountered during iteration are excluded.

constallKeys=pad.keys();// ['step', 'user', 'result']

Returns:string[]

entries()

Returns an array of [key, ScratchpadEntry] tuples for all non-expired entries.

for(const[key,entry]ofpad.entries()){console.log(key,entry.value,entry.tags);}

Returns:[string, ScratchpadEntry][]

findByTag(tag)

Returns all non-expired entries whose tags array includes the given tag (exact match).

pad.set('london','UK capital',{tags: ['geo','important']});pad.set('paris','France capital',{tags: ['geo']});constgeoEntries=pad.findByTag('geo');// both entries

Parameters:

  • tag (string) -- The tag to search for.

Returns:ScratchpadEntry[]

namespace(name)

Returns a scoped view of the scratchpad where all operations are prefixed with name:. Namespaces share the underlying storage with the parent -- they are views, not copies. Namespaces can be nested.

constmemory=pad.namespace('memory');memory.set('fact','The sky is blue');memory.get('fact');// 'The sky is blue'pad.get('memory:fact');// 'The sky is blue'// Nested namespaces compose prefixesconstdeep=pad.namespace('a').namespace('b');deep.set('key','val');pad.get('a:b:key');// 'val'// Namespace-scoped operationsmemory.keys();// ['fact'] (prefix stripped)memory.clear();// removes only memory:* entries

Parameters:

  • name (string) -- The namespace prefix.

Returns:Scratchpad -- A namespace-scoped scratchpad instance with the same full API.

snapshot()

Captures the full scratchpad state at the current point in time. The returned snapshot is a plain object suitable for serialization.

constsnap=pad.snapshot();// { entries: { ... }, timestamp: 1710000000000, version: 1 }

Returns:ScratchpadSnapshot

restore(snapshot)

Replaces the entire scratchpad state with the contents of a snapshot. Clears all existing entries before restoring. Throws ScratchpadVersionError if the snapshot version is not supported.

pad.restore(snap);

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot previously obtained from snapshot() or serialize().

Returns:void

Throws:ScratchpadVersionError if snapshot.version is not 1.

serialize()

Alias for snapshot(). Returns the same ScratchpadSnapshot structure.

constdata=pad.serialize();

Returns:ScratchpadSnapshot

toContext(options?)

Renders scratchpad contents as a formatted string suitable for injection into an LLM prompt. Supports filtering by tags or namespace, multiple output formats, token budget limits, and custom headers.

pad.set('name','Alice');pad.set('role','admin');pad.toContext();// 'name: Alice\nrole: admin'pad.toContext({format: 'markdown'});// '## name\nAlice\n\n## role\nadmin'pad.toContext({format: 'json'});// '{"name":"Alice","role":"admin"}'pad.toContext({format: 'xml'});// '<entry key="name">Alice</entry>\n<entry key="role">admin</entry>'

Parameters:

OptionTypeDefaultDescription
format'kv' | 'markdown' | 'xml' | 'json''kv'Output format.
filterTagsstring[]undefinedOnly include entries that have at least one of the specified tags.
filterNamespacestringundefinedOnly include entries whose key starts with the given namespace prefix.
maxTokensnumberundefinedTruncate output to fit within this token budget.
tokenCounter(text: string) => numbertext.lengthFunction to count tokens. Used with maxTokens.
includeMetadatabooleanundefinedReserved for future use.
headerstringundefinedText prepended to the output before the formatted entries.

Returns:string

stats()

Returns aggregate statistics about the scratchpad's current state.

constst=pad.stats();// {// size: 3, // live (non-expired) entry count// rawSize: 4, // total entries including expired-not-yet-swept// namespaceCount: 2,// namespaces: ['ctx', 'mem'],// entriesWithTtl: 1,// tagCounts: { geo: 2, important: 1 },// oldestEntryAt: 1710000000000,// newestEntryAt: 1710000001000,// }

Returns:ScratchpadStats

FieldTypeDescription
sizenumberCount of non-expired entries.
rawSizenumberTotal entries in the store, including expired entries not yet swept.
namespaceCountnumberNumber of distinct namespace prefixes.
namespacesstring[]List of distinct namespace prefixes.
entriesWithTtlnumberCount of entries that have a TTL set.
tagCountsRecord<string, number>Count of entries per tag.
oldestEntryAtnumber | nullcreatedAt of the oldest live entry, or null if empty.
newestEntryAtnumber | nullcreatedAt of the newest live entry, or null if empty.

on(event, handler)

Registers an event handler. Returns an unsubscribe function.

constunsub=pad.on('set',({ key, entry, isUpdate })=>{console.log(isUpdate ? 'updated' : 'created',key);});pad.on('delete',({ key, entry })=>{console.log('deleted',key);});pad.on('expire',({ key, entry })=>{console.log('expired',key);});pad.on('clear',({ count })=>{console.log('cleared',count,'entries');});// Stop listeningunsub();

Parameters:

  • event (ScratchpadEventName) -- One of 'set', 'delete', 'expire', 'clear'.
  • handler (ScratchpadEventHandler<K>) -- Callback receiving the event payload.

Event Payloads:

EventPayload
set{ key: string; entry: ScratchpadEntry; isUpdate: boolean }
delete{ key: string; entry: ScratchpadEntry }
expire{ key: string; entry: ScratchpadEntry }
clear{ count: number }

Returns:() => void -- Call to unsubscribe.

save()

Persists the current scratchpad state using the configured PersistenceAdapter. No-op if no adapter was provided.

awaitpad.save();

Returns:Promise<void>

load()

Loads scratchpad state from the configured PersistenceAdapter and restores it. No-op if no adapter was provided or the adapter returns null.

awaitpad.load();

Returns:Promise<void>

destroy()

Cleans up resources. Stops the background sweep timer if one is running.

awaitpad.destroy();

Returns:Promise<void>


TTL Utility Functions

isExpired(entry, now)

Determines whether a scratchpad entry has expired based on its TTL configuration.

import{isExpired}from'agent-scratchpad';constexpired=isExpired(entry,Date.now());

Parameters:

  • entry (ScratchpadEntry) -- The entry to check.
  • now (number) -- Current timestamp in milliseconds.

Returns:boolean -- true if the entry's TTL has elapsed.

Logic:

  • Returns false if entry.ttl is null.
  • For fixed TTL (slidingTtl: false): expired when now >= entry.createdAt + entry.ttl.
  • For sliding TTL (slidingTtl: true): expired when now >= entry.accessedAt + entry.ttl.

expiresAt(entry)

Calculates the absolute expiration timestamp for an entry.

import{expiresAt}from'agent-scratchpad';constexpiry=expiresAt(entry);// number | null

Parameters:

  • entry (ScratchpadEntry) -- The entry to inspect.

Returns:number | null -- The Unix timestamp (ms) when the entry expires, or null if it has no TTL.


Types

ScratchpadEntry<T>

interfaceScratchpadEntry<T=unknown>{key: string;value: T;createdAt: number;// Unix ms when first createdupdatedAt: number;// Unix ms when value last updatedaccessedAt: number;// Unix ms when last read via get()ttl: number|null;// TTL in ms, null = no expirationslidingTtl: boolean;// true = TTL resets on accesstags: string[];// string labels for categorization}

EntryOptions

interfaceEntryOptions{ttl?: number|null;slidingTtl?: boolean;tags?: string[];}

ScratchpadOptions

interfaceScratchpadOptions{defaultTtl?: number|null;defaultSlidingTtl?: boolean;sweepIntervalMs?: number|null;now?: ()=>number;persistence?: PersistenceAdapter;}

ScratchpadSnapshot

interfaceScratchpadSnapshot{entries: Record<string,ScratchpadEntry>;timestamp: number;version: 1;}

ScratchpadStats

interfaceScratchpadStats{size: number;rawSize: number;namespaceCount: number;namespaces: string[];entriesWithTtl: number;tagCounts: Record<string,number>;oldestEntryAt: number|null;newestEntryAt: number|null;}

ToContextOptions

interfaceToContextOptions{format?: 'markdown'|'xml'|'json'|'kv';filterTags?: string[];filterNamespace?: string;maxTokens?: number;tokenCounter?: (text: string)=>number;includeMetadata?: boolean;header?: string;}

PersistenceAdapter

interfacePersistenceAdapter{load(): Promise<ScratchpadSnapshot|null>;save(snap: ScratchpadSnapshot): Promise<void>;}

ScratchpadEvents

interfaceScratchpadEvents{set: {key: string;entry: ScratchpadEntry;isUpdate: boolean};delete: {key: string;entry: ScratchpadEntry};expire: {key: string;entry: ScratchpadEntry};clear: {count: number};}

ScratchpadEventName

typeScratchpadEventName='set'|'delete'|'expire'|'clear';

ScratchpadEventHandler<K>

typeScratchpadEventHandler<KextendsScratchpadEventName>=(data: ScratchpadEvents[K])=>void;

Error Handling

agent-scratchpad exports three error classes, all extending a common base.

ScratchpadError

Base class for all scratchpad errors. Extends Error with a code property.

import{ScratchpadError}from'agent-scratchpad';try{pad.restore(badSnapshot);}catch(err){if(errinstanceofScratchpadError){console.error(err.code);// e.g. 'SCRATCHPAD_VERSION_ERROR'console.error(err.message);// human-readable description}}
PropertyTypeDescription
codestringMachine-readable error code.
messagestringHuman-readable error description.
namestringAlways 'ScratchpadError'.

ScratchpadConfigError

Thrown when invalid configuration is provided to createScratchpad().

  • Code:SCRATCHPAD_CONFIG_ERROR

ScratchpadVersionError

Thrown when restore() encounters a snapshot with an unsupported version number.

  • Code:SCRATCHPAD_VERSION_ERROR
  • Additional property:version (number) -- The unsupported version that was encountered.
import{ScratchpadVersionError}from'agent-scratchpad';try{pad.restore(snap);}catch(err){if(errinstanceofScratchpadVersionError){console.error(`Unsupported version: ${err.version}`);}}

Advanced Usage

Agent Working Memory in a ReAct Loop

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 300_000});// 5-minute default// Step 1: Store tool outputpad.set('search:result',apiResponse,{tags: ['tool-result','search']});// Step 2: Extract and store entitiespad.set('entities:user',{name: 'Alice',id: 42},{tags: ['entity']});pad.set('entities:order',{orderId: '#12345'},{tags: ['entity']});// Step 3: Inject scratchpad into promptconstagentContext=pad.toContext({format: 'markdown',header: '## Agent Working Memory',filterTags: ['entity'],});// Produces:// ## Agent Working Memory// ## entities:user// [object Object]// ...

Namespace Isolation for Multi-Agent Systems

constpad=createScratchpad();constagent1=pad.namespace('agent1');constagent2=pad.namespace('agent2');agent1.set('plan','Research the topic');agent2.set('plan','Draft the response');// Each agent sees only its own entriesagent1.keys();// ['plan']agent2.keys();// ['plan']// Parent sees all entries with prefixed keyspad.keys();// ['agent1:plan', 'agent2:plan']// Clear one agent without affecting the otheragent1.clear();agent2.keys();// ['plan'] -- unaffected

Sliding TTL for Session-Like Data

constpad=createScratchpad();// Session token stays alive as long as the agent keeps accessing itpad.set('session',{token: 'abc123'},{ttl: 30_000,slidingTtl: true});// Each access resets the 30-second expiration windowpad.get('session');// resets timerpad.get('session');// resets timer again// If 30 seconds pass without access, the entry expires

Background Sweep with Event Logging

constpad=createScratchpad({sweepIntervalMs: 10_000});pad.on('expire',({ key, entry })=>{console.log(`Expired: ${key} (created ${newDate(entry.createdAt).toISOString()})`);});pad.set('temp','data',{ttl: 15_000});// The sweep timer runs every 10 seconds and removes expired entries.// The expire event fires for each entry removed by the sweep.// Stop the sweep timer when doneawaitpad.destroy();

Persistence with a File-Based Adapter

import{createScratchpad,PersistenceAdapter,ScratchpadSnapshot}from'agent-scratchpad';importfsfrom'fs/promises';constfileAdapter: PersistenceAdapter={asyncload(){try{constdata=awaitfs.readFile('scratchpad.json','utf8');returnJSON.parse(data)asScratchpadSnapshot;}catch{returnnull;}},asyncsave(snap){awaitfs.writeFile('scratchpad.json',JSON.stringify(snap,null,2));},};constpad=createScratchpad({persistence: fileAdapter});// Restore previous state on startupawaitpad.load();// Work with the scratchpadpad.set('progress','step-3');// Persist state before shutdownawaitpad.save();

Snapshot-Based Backtracking

constpad=createScratchpad();pad.set('approach','strategy-A');pad.set('findings',['result-1']);// Save state before trying something riskyconstcheckpoint=pad.snapshot();pad.set('approach','strategy-B');pad.set('findings',['result-1','result-2-failed']);// Strategy B failed -- roll backpad.restore(checkpoint);pad.get('approach');// 'strategy-A'

Token-Budget-Aware Context Rendering

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();pad.set('summary','A long summary of findings...');pad.set('details','Extensive details that may not fit...');// Use a custom token counter (e.g., tiktoken)constcontext=pad.toContext({format: 'kv',maxTokens: 500,tokenCounter: (text)=>Math.ceil(text.length/4),// rough estimateheader: '## Working Memory',});

Deterministic Testing with Custom Time

import{createScratchpad}from'agent-scratchpad';letnow=0;constpad=createScratchpad({now: ()=>now});pad.set('key','value',{ttl: 100});now=50;pad.get('key');// 'value' -- still alivenow=100;pad.get('key');// undefined -- expired exactly at 100ms

TypeScript

agent-scratchpad is written in TypeScript and ships with full type declarations. All exported functions, interfaces, and types are available for import:

import{createScratchpad,fromSnapshot,toContext,isExpired,expiresAt,ScratchpadError,ScratchpadConfigError,ScratchpadVersionError,}from'agent-scratchpad';importtype{Scratchpad,ScratchpadEntry,ScratchpadOptions,ScratchpadSnapshot,ScratchpadStats,ToContextOptions,EntryOptions,ScratchpadEventName,ScratchpadEventHandler,ScratchpadEvents,PersistenceAdapter,}from'agent-scratchpad';

Generic type parameters on get<T>() and set<T>() provide type-safe value access without casts:

interfaceUser{id: number;name: string;}pad.set<User>('user',{id: 1,name: 'Alice'});constuser=pad.get<User>('user');// user is User | undefined

License

MIT

About

Lightweight key-value scratchpad for agent reasoning

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - SiluPanda/agent-scratchpad: Lightweight key-value scratchpad for agent reasoning · GitHub
Skip to content

Repository files navigation

agent-scratchpad

Lightweight key-value scratchpad for AI agent working memory.

npm versionnpm downloadslicensenodeTypeScript

agent-scratchpad is a zero-dependency, in-process key-value store purpose-built for AI agent reasoning loops. Agents executing multi-step workflows (ReAct, Plan-and-Execute, Chain-of-Thought with tool use) need a place to write down intermediate state between steps -- tool outputs, extracted entities, partial computations, decision rationale, and task decomposition state. This package provides that working memory with typed entries, automatic TTL-based expiration, hierarchical namespaces, tag-based querying, point-in-time snapshots, event-driven change observation, pluggable persistence, and a toContext() method that renders scratchpad contents directly into LLM prompts. It works with any agent framework or custom agent loop.

Installation

npm install agent-scratchpad

Quick Start

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();// Store intermediate resultspad.set('step','analyze');pad.set('user',{id: 42,name: 'Alice'});console.log(pad.get('step'));// 'analyze'console.log(pad.has('user'));// trueconsole.log(pad.keys());// ['step', 'user']// Render contents for an LLM promptconstcontext=pad.toContext({format: 'markdown'});

Features

  • Zero runtime dependencies -- all logic uses built-in JavaScript APIs
  • TypeScript-first -- full generic type safety on get<T>() and set<T>()
  • TTL expiration -- fixed or sliding time-to-live with lazy and active sweep modes
  • Hierarchical namespaces -- scope entries per agent, task, or step with pad.namespace('name')
  • Tag-based querying -- label entries and retrieve them with findByTag()
  • Snapshots -- capture and restore full scratchpad state for backtracking and debugging
  • Context rendering -- format entries as Markdown, XML, JSON, or key-value pairs for LLM prompts
  • Event system -- observe set, delete, expire, and clear events
  • Pluggable persistence -- save and load scratchpad state via a simple adapter interface
  • Framework-agnostic -- works with LangChain, Vercel AI SDK, AutoGen, CrewAI, or any custom loop

API Reference

createScratchpad(options?)

Creates a new Scratchpad instance.

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 60_000,defaultSlidingTtl: false,sweepIntervalMs: 10_000,now: ()=>Date.now(),persistence: adapter,});

Parameters:

OptionTypeDefaultDescription
defaultTtlnumber | nullnullDefault TTL in milliseconds applied to entries that do not specify their own. null means no expiration.
defaultSlidingTtlbooleanfalseWhether the default TTL mode is sliding (resets on access) or fixed (from creation).
sweepIntervalMsnumber | nullnullInterval in milliseconds for proactive background sweep of expired entries. null disables active sweep.
now() => numberDate.nowCustom time source. Useful for deterministic testing.
persistencePersistenceAdapterundefinedOptional adapter for saving and loading scratchpad state.

Returns:Scratchpad


fromSnapshot(snapshot, options?)

Creates a new Scratchpad pre-populated from a previously captured snapshot.

import{fromSnapshot}from'agent-scratchpad';constpad=fromSnapshot(snap,{defaultTtl: 30_000});

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot object previously obtained from pad.snapshot() or pad.serialize().
  • options (ScratchpadOptions, optional) -- Configuration options passed to the underlying createScratchpad() call.

Returns:Scratchpad


Scratchpad Methods

set<T>(key, value, options?)

Stores a value under the given key. If the key already exists, updates the value and updatedAt timestamp while preserving createdAt.

pad.set('result',{score: 0.95});pad.set('cache','value',{ttl: 5_000,tags: ['temporary']});pad.set('session',token,{ttl: 30_000,slidingTtl: true});

Parameters:

  • key (string) -- The entry key.
  • value (T) -- The value to store.
  • options (EntryOptions, optional) -- Per-entry configuration.
OptionTypeDefaultDescription
ttlnumber | nullInherits defaultTtlTTL in milliseconds. null disables expiration for this entry.
slidingTtlbooleanInherits defaultSlidingTtlWhether TTL resets on each get() access.
tagsstring[][]String labels for categorizing the entry.

Returns:void

get<T>(key)

Retrieves the value for a key. If the entry has expired, it is removed, an expire event fires, and undefined is returned. On a successful read, accessedAt is updated (which resets the sliding TTL window if applicable).

constuser=pad.get<{id: number;name: string}>('user');

Parameters:

  • key (string) -- The entry key.

Returns:T | undefined

has(key)

Checks whether a key exists and is not expired. Expired entries are removed and trigger an expire event.

if(pad.has('apiResponse')){// entry is live}

Parameters:

  • key (string) -- The entry key.

Returns:boolean

delete(key)

Removes an entry by key. Fires a delete event if the entry existed.

constremoved=pad.delete('staleData');// true if it existed

Parameters:

  • key (string) -- The entry key.

Returns:boolean -- true if the entry existed and was removed, false otherwise.

clear()

Removes all entries from the scratchpad. Fires a clear event with the count of removed entries.

pad.clear();

Returns:void

keys()

Returns an array of all non-expired keys. Expired entries encountered during iteration are excluded.

constallKeys=pad.keys();// ['step', 'user', 'result']

Returns:string[]

entries()

Returns an array of [key, ScratchpadEntry] tuples for all non-expired entries.

for(const[key,entry]ofpad.entries()){console.log(key,entry.value,entry.tags);}

Returns:[string, ScratchpadEntry][]

findByTag(tag)

Returns all non-expired entries whose tags array includes the given tag (exact match).

pad.set('london','UK capital',{tags: ['geo','important']});pad.set('paris','France capital',{tags: ['geo']});constgeoEntries=pad.findByTag('geo');// both entries

Parameters:

  • tag (string) -- The tag to search for.

Returns:ScratchpadEntry[]

namespace(name)

Returns a scoped view of the scratchpad where all operations are prefixed with name:. Namespaces share the underlying storage with the parent -- they are views, not copies. Namespaces can be nested.

constmemory=pad.namespace('memory');memory.set('fact','The sky is blue');memory.get('fact');// 'The sky is blue'pad.get('memory:fact');// 'The sky is blue'// Nested namespaces compose prefixesconstdeep=pad.namespace('a').namespace('b');deep.set('key','val');pad.get('a:b:key');// 'val'// Namespace-scoped operationsmemory.keys();// ['fact'] (prefix stripped)memory.clear();// removes only memory:* entries

Parameters:

  • name (string) -- The namespace prefix.

Returns:Scratchpad -- A namespace-scoped scratchpad instance with the same full API.

snapshot()

Captures the full scratchpad state at the current point in time. The returned snapshot is a plain object suitable for serialization.

constsnap=pad.snapshot();// { entries: { ... }, timestamp: 1710000000000, version: 1 }

Returns:ScratchpadSnapshot

restore(snapshot)

Replaces the entire scratchpad state with the contents of a snapshot. Clears all existing entries before restoring. Throws ScratchpadVersionError if the snapshot version is not supported.

pad.restore(snap);

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot previously obtained from snapshot() or serialize().

Returns:void

Throws:ScratchpadVersionError if snapshot.version is not 1.

serialize()

Alias for snapshot(). Returns the same ScratchpadSnapshot structure.

constdata=pad.serialize();

Returns:ScratchpadSnapshot

toContext(options?)

Renders scratchpad contents as a formatted string suitable for injection into an LLM prompt. Supports filtering by tags or namespace, multiple output formats, token budget limits, and custom headers.

pad.set('name','Alice');pad.set('role','admin');pad.toContext();// 'name: Alice\nrole: admin'pad.toContext({format: 'markdown'});// '## name\nAlice\n\n## role\nadmin'pad.toContext({format: 'json'});// '{"name":"Alice","role":"admin"}'pad.toContext({format: 'xml'});// '<entry key="name">Alice</entry>\n<entry key="role">admin</entry>'

Parameters:

OptionTypeDefaultDescription
format'kv' | 'markdown' | 'xml' | 'json''kv'Output format.
filterTagsstring[]undefinedOnly include entries that have at least one of the specified tags.
filterNamespacestringundefinedOnly include entries whose key starts with the given namespace prefix.
maxTokensnumberundefinedTruncate output to fit within this token budget.
tokenCounter(text: string) => numbertext.lengthFunction to count tokens. Used with maxTokens.
includeMetadatabooleanundefinedReserved for future use.
headerstringundefinedText prepended to the output before the formatted entries.

Returns:string

stats()

Returns aggregate statistics about the scratchpad's current state.

constst=pad.stats();// {// size: 3, // live (non-expired) entry count// rawSize: 4, // total entries including expired-not-yet-swept// namespaceCount: 2,// namespaces: ['ctx', 'mem'],// entriesWithTtl: 1,// tagCounts: { geo: 2, important: 1 },// oldestEntryAt: 1710000000000,// newestEntryAt: 1710000001000,// }

Returns:ScratchpadStats

FieldTypeDescription
sizenumberCount of non-expired entries.
rawSizenumberTotal entries in the store, including expired entries not yet swept.
namespaceCountnumberNumber of distinct namespace prefixes.
namespacesstring[]List of distinct namespace prefixes.
entriesWithTtlnumberCount of entries that have a TTL set.
tagCountsRecord<string, number>Count of entries per tag.
oldestEntryAtnumber | nullcreatedAt of the oldest live entry, or null if empty.
newestEntryAtnumber | nullcreatedAt of the newest live entry, or null if empty.

on(event, handler)

Registers an event handler. Returns an unsubscribe function.

constunsub=pad.on('set',({ key, entry, isUpdate })=>{console.log(isUpdate ? 'updated' : 'created',key);});pad.on('delete',({ key, entry })=>{console.log('deleted',key);});pad.on('expire',({ key, entry })=>{console.log('expired',key);});pad.on('clear',({ count })=>{console.log('cleared',count,'entries');});// Stop listeningunsub();

Parameters:

  • event (ScratchpadEventName) -- One of 'set', 'delete', 'expire', 'clear'.
  • handler (ScratchpadEventHandler<K>) -- Callback receiving the event payload.

Event Payloads:

EventPayload
set{ key: string; entry: ScratchpadEntry; isUpdate: boolean }
delete{ key: string; entry: ScratchpadEntry }
expire{ key: string; entry: ScratchpadEntry }
clear{ count: number }

Returns:() => void -- Call to unsubscribe.

save()

Persists the current scratchpad state using the configured PersistenceAdapter. No-op if no adapter was provided.

awaitpad.save();

Returns:Promise<void>

load()

Loads scratchpad state from the configured PersistenceAdapter and restores it. No-op if no adapter was provided or the adapter returns null.

awaitpad.load();

Returns:Promise<void>

destroy()

Cleans up resources. Stops the background sweep timer if one is running.

awaitpad.destroy();

Returns:Promise<void>


TTL Utility Functions

isExpired(entry, now)

Determines whether a scratchpad entry has expired based on its TTL configuration.

import{isExpired}from'agent-scratchpad';constexpired=isExpired(entry,Date.now());

Parameters:

  • entry (ScratchpadEntry) -- The entry to check.
  • now (number) -- Current timestamp in milliseconds.

Returns:boolean -- true if the entry's TTL has elapsed.

Logic:

  • Returns false if entry.ttl is null.
  • For fixed TTL (slidingTtl: false): expired when now >= entry.createdAt + entry.ttl.
  • For sliding TTL (slidingTtl: true): expired when now >= entry.accessedAt + entry.ttl.

expiresAt(entry)

Calculates the absolute expiration timestamp for an entry.

import{expiresAt}from'agent-scratchpad';constexpiry=expiresAt(entry);// number | null

Parameters:

  • entry (ScratchpadEntry) -- The entry to inspect.

Returns:number | null -- The Unix timestamp (ms) when the entry expires, or null if it has no TTL.


Types

ScratchpadEntry<T>

interfaceScratchpadEntry<T=unknown>{key: string;value: T;createdAt: number;// Unix ms when first createdupdatedAt: number;// Unix ms when value last updatedaccessedAt: number;// Unix ms when last read via get()ttl: number|null;// TTL in ms, null = no expirationslidingTtl: boolean;// true = TTL resets on accesstags: string[];// string labels for categorization}

EntryOptions

interfaceEntryOptions{ttl?: number|null;slidingTtl?: boolean;tags?: string[];}

ScratchpadOptions

interfaceScratchpadOptions{defaultTtl?: number|null;defaultSlidingTtl?: boolean;sweepIntervalMs?: number|null;now?: ()=>number;persistence?: PersistenceAdapter;}

ScratchpadSnapshot

interfaceScratchpadSnapshot{entries: Record<string,ScratchpadEntry>;timestamp: number;version: 1;}

ScratchpadStats

interfaceScratchpadStats{size: number;rawSize: number;namespaceCount: number;namespaces: string[];entriesWithTtl: number;tagCounts: Record<string,number>;oldestEntryAt: number|null;newestEntryAt: number|null;}

ToContextOptions

interfaceToContextOptions{format?: 'markdown'|'xml'|'json'|'kv';filterTags?: string[];filterNamespace?: string;maxTokens?: number;tokenCounter?: (text: string)=>number;includeMetadata?: boolean;header?: string;}

PersistenceAdapter

interfacePersistenceAdapter{load(): Promise<ScratchpadSnapshot|null>;save(snap: ScratchpadSnapshot): Promise<void>;}

ScratchpadEvents

interfaceScratchpadEvents{set: {key: string;entry: ScratchpadEntry;isUpdate: boolean};delete: {key: string;entry: ScratchpadEntry};expire: {key: string;entry: ScratchpadEntry};clear: {count: number};}

ScratchpadEventName

typeScratchpadEventName='set'|'delete'|'expire'|'clear';

ScratchpadEventHandler<K>

typeScratchpadEventHandler<KextendsScratchpadEventName>=(data: ScratchpadEvents[K])=>void;

Error Handling

agent-scratchpad exports three error classes, all extending a common base.

ScratchpadError

Base class for all scratchpad errors. Extends Error with a code property.

import{ScratchpadError}from'agent-scratchpad';try{pad.restore(badSnapshot);}catch(err){if(errinstanceofScratchpadError){console.error(err.code);// e.g. 'SCRATCHPAD_VERSION_ERROR'console.error(err.message);// human-readable description}}
PropertyTypeDescription
codestringMachine-readable error code.
messagestringHuman-readable error description.
namestringAlways 'ScratchpadError'.

ScratchpadConfigError

Thrown when invalid configuration is provided to createScratchpad().

  • Code:SCRATCHPAD_CONFIG_ERROR

ScratchpadVersionError

Thrown when restore() encounters a snapshot with an unsupported version number.

  • Code:SCRATCHPAD_VERSION_ERROR
  • Additional property:version (number) -- The unsupported version that was encountered.
import{ScratchpadVersionError}from'agent-scratchpad';try{pad.restore(snap);}catch(err){if(errinstanceofScratchpadVersionError){console.error(`Unsupported version: ${err.version}`);}}

Advanced Usage

Agent Working Memory in a ReAct Loop

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 300_000});// 5-minute default// Step 1: Store tool outputpad.set('search:result',apiResponse,{tags: ['tool-result','search']});// Step 2: Extract and store entitiespad.set('entities:user',{name: 'Alice',id: 42},{tags: ['entity']});pad.set('entities:order',{orderId: '#12345'},{tags: ['entity']});// Step 3: Inject scratchpad into promptconstagentContext=pad.toContext({format: 'markdown',header: '## Agent Working Memory',filterTags: ['entity'],});// Produces:// ## Agent Working Memory// ## entities:user// [object Object]// ...

Namespace Isolation for Multi-Agent Systems

constpad=createScratchpad();constagent1=pad.namespace('agent1');constagent2=pad.namespace('agent2');agent1.set('plan','Research the topic');agent2.set('plan','Draft the response');// Each agent sees only its own entriesagent1.keys();// ['plan']agent2.keys();// ['plan']// Parent sees all entries with prefixed keyspad.keys();// ['agent1:plan', 'agent2:plan']// Clear one agent without affecting the otheragent1.clear();agent2.keys();// ['plan'] -- unaffected

Sliding TTL for Session-Like Data

constpad=createScratchpad();// Session token stays alive as long as the agent keeps accessing itpad.set('session',{token: 'abc123'},{ttl: 30_000,slidingTtl: true});// Each access resets the 30-second expiration windowpad.get('session');// resets timerpad.get('session');// resets timer again// If 30 seconds pass without access, the entry expires

Background Sweep with Event Logging

constpad=createScratchpad({sweepIntervalMs: 10_000});pad.on('expire',({ key, entry })=>{console.log(`Expired: ${key} (created ${newDate(entry.createdAt).toISOString()})`);});pad.set('temp','data',{ttl: 15_000});// The sweep timer runs every 10 seconds and removes expired entries.// The expire event fires for each entry removed by the sweep.// Stop the sweep timer when doneawaitpad.destroy();

Persistence with a File-Based Adapter

import{createScratchpad,PersistenceAdapter,ScratchpadSnapshot}from'agent-scratchpad';importfsfrom'fs/promises';constfileAdapter: PersistenceAdapter={asyncload(){try{constdata=awaitfs.readFile('scratchpad.json','utf8');returnJSON.parse(data)asScratchpadSnapshot;}catch{returnnull;}},asyncsave(snap){awaitfs.writeFile('scratchpad.json',JSON.stringify(snap,null,2));},};constpad=createScratchpad({persistence: fileAdapter});// Restore previous state on startupawaitpad.load();// Work with the scratchpadpad.set('progress','step-3');// Persist state before shutdownawaitpad.save();

Snapshot-Based Backtracking

constpad=createScratchpad();pad.set('approach','strategy-A');pad.set('findings',['result-1']);// Save state before trying something riskyconstcheckpoint=pad.snapshot();pad.set('approach','strategy-B');pad.set('findings',['result-1','result-2-failed']);// Strategy B failed -- roll backpad.restore(checkpoint);pad.get('approach');// 'strategy-A'

Token-Budget-Aware Context Rendering

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();pad.set('summary','A long summary of findings...');pad.set('details','Extensive details that may not fit...');// Use a custom token counter (e.g., tiktoken)constcontext=pad.toContext({format: 'kv',maxTokens: 500,tokenCounter: (text)=>Math.ceil(text.length/4),// rough estimateheader: '## Working Memory',});

Deterministic Testing with Custom Time

import{createScratchpad}from'agent-scratchpad';letnow=0;constpad=createScratchpad({now: ()=>now});pad.set('key','value',{ttl: 100});now=50;pad.get('key');// 'value' -- still alivenow=100;pad.get('key');// undefined -- expired exactly at 100ms

TypeScript

agent-scratchpad is written in TypeScript and ships with full type declarations. All exported functions, interfaces, and types are available for import:

import{createScratchpad,fromSnapshot,toContext,isExpired,expiresAt,ScratchpadError,ScratchpadConfigError,ScratchpadVersionError,}from'agent-scratchpad';importtype{Scratchpad,ScratchpadEntry,ScratchpadOptions,ScratchpadSnapshot,ScratchpadStats,ToContextOptions,EntryOptions,ScratchpadEventName,ScratchpadEventHandler,ScratchpadEvents,PersistenceAdapter,}from'agent-scratchpad';

Generic type parameters on get<T>() and set<T>() provide type-safe value access without casts:

interfaceUser{id: number;name: string;}pad.set<User>('user',{id: 1,name: 'Alice'});constuser=pad.get<User>('user');// user is User | undefined

License

MIT

About

Lightweight key-value scratchpad for agent reasoning

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SiluPanda/agent-scratchpad: Lightweight key-value scratchpad for agent reasoning · GitHub
Skip to content

Repository files navigation

agent-scratchpad

Lightweight key-value scratchpad for AI agent working memory.

npm versionnpm downloadslicensenodeTypeScript

agent-scratchpad is a zero-dependency, in-process key-value store purpose-built for AI agent reasoning loops. Agents executing multi-step workflows (ReAct, Plan-and-Execute, Chain-of-Thought with tool use) need a place to write down intermediate state between steps -- tool outputs, extracted entities, partial computations, decision rationale, and task decomposition state. This package provides that working memory with typed entries, automatic TTL-based expiration, hierarchical namespaces, tag-based querying, point-in-time snapshots, event-driven change observation, pluggable persistence, and a toContext() method that renders scratchpad contents directly into LLM prompts. It works with any agent framework or custom agent loop.

Installation

npm install agent-scratchpad

Quick Start

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();// Store intermediate resultspad.set('step','analyze');pad.set('user',{id: 42,name: 'Alice'});console.log(pad.get('step'));// 'analyze'console.log(pad.has('user'));// trueconsole.log(pad.keys());// ['step', 'user']// Render contents for an LLM promptconstcontext=pad.toContext({format: 'markdown'});

Features

  • Zero runtime dependencies -- all logic uses built-in JavaScript APIs
  • TypeScript-first -- full generic type safety on get<T>() and set<T>()
  • TTL expiration -- fixed or sliding time-to-live with lazy and active sweep modes
  • Hierarchical namespaces -- scope entries per agent, task, or step with pad.namespace('name')
  • Tag-based querying -- label entries and retrieve them with findByTag()
  • Snapshots -- capture and restore full scratchpad state for backtracking and debugging
  • Context rendering -- format entries as Markdown, XML, JSON, or key-value pairs for LLM prompts
  • Event system -- observe set, delete, expire, and clear events
  • Pluggable persistence -- save and load scratchpad state via a simple adapter interface
  • Framework-agnostic -- works with LangChain, Vercel AI SDK, AutoGen, CrewAI, or any custom loop

API Reference

createScratchpad(options?)

Creates a new Scratchpad instance.

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 60_000,defaultSlidingTtl: false,sweepIntervalMs: 10_000,now: ()=>Date.now(),persistence: adapter,});

Parameters:

OptionTypeDefaultDescription
defaultTtlnumber | nullnullDefault TTL in milliseconds applied to entries that do not specify their own. null means no expiration.
defaultSlidingTtlbooleanfalseWhether the default TTL mode is sliding (resets on access) or fixed (from creation).
sweepIntervalMsnumber | nullnullInterval in milliseconds for proactive background sweep of expired entries. null disables active sweep.
now() => numberDate.nowCustom time source. Useful for deterministic testing.
persistencePersistenceAdapterundefinedOptional adapter for saving and loading scratchpad state.

Returns:Scratchpad


fromSnapshot(snapshot, options?)

Creates a new Scratchpad pre-populated from a previously captured snapshot.

import{fromSnapshot}from'agent-scratchpad';constpad=fromSnapshot(snap,{defaultTtl: 30_000});

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot object previously obtained from pad.snapshot() or pad.serialize().
  • options (ScratchpadOptions, optional) -- Configuration options passed to the underlying createScratchpad() call.

Returns:Scratchpad


Scratchpad Methods

set<T>(key, value, options?)

Stores a value under the given key. If the key already exists, updates the value and updatedAt timestamp while preserving createdAt.

pad.set('result',{score: 0.95});pad.set('cache','value',{ttl: 5_000,tags: ['temporary']});pad.set('session',token,{ttl: 30_000,slidingTtl: true});

Parameters:

  • key (string) -- The entry key.
  • value (T) -- The value to store.
  • options (EntryOptions, optional) -- Per-entry configuration.
OptionTypeDefaultDescription
ttlnumber | nullInherits defaultTtlTTL in milliseconds. null disables expiration for this entry.
slidingTtlbooleanInherits defaultSlidingTtlWhether TTL resets on each get() access.
tagsstring[][]String labels for categorizing the entry.

Returns:void

get<T>(key)

Retrieves the value for a key. If the entry has expired, it is removed, an expire event fires, and undefined is returned. On a successful read, accessedAt is updated (which resets the sliding TTL window if applicable).

constuser=pad.get<{id: number;name: string}>('user');

Parameters:

  • key (string) -- The entry key.

Returns:T | undefined

has(key)

Checks whether a key exists and is not expired. Expired entries are removed and trigger an expire event.

if(pad.has('apiResponse')){// entry is live}

Parameters:

  • key (string) -- The entry key.

Returns:boolean

delete(key)

Removes an entry by key. Fires a delete event if the entry existed.

constremoved=pad.delete('staleData');// true if it existed

Parameters:

  • key (string) -- The entry key.

Returns:boolean -- true if the entry existed and was removed, false otherwise.

clear()

Removes all entries from the scratchpad. Fires a clear event with the count of removed entries.

pad.clear();

Returns:void

keys()

Returns an array of all non-expired keys. Expired entries encountered during iteration are excluded.

constallKeys=pad.keys();// ['step', 'user', 'result']

Returns:string[]

entries()

Returns an array of [key, ScratchpadEntry] tuples for all non-expired entries.

for(const[key,entry]ofpad.entries()){console.log(key,entry.value,entry.tags);}

Returns:[string, ScratchpadEntry][]

findByTag(tag)

Returns all non-expired entries whose tags array includes the given tag (exact match).

pad.set('london','UK capital',{tags: ['geo','important']});pad.set('paris','France capital',{tags: ['geo']});constgeoEntries=pad.findByTag('geo');// both entries

Parameters:

  • tag (string) -- The tag to search for.

Returns:ScratchpadEntry[]

namespace(name)

Returns a scoped view of the scratchpad where all operations are prefixed with name:. Namespaces share the underlying storage with the parent -- they are views, not copies. Namespaces can be nested.

constmemory=pad.namespace('memory');memory.set('fact','The sky is blue');memory.get('fact');// 'The sky is blue'pad.get('memory:fact');// 'The sky is blue'// Nested namespaces compose prefixesconstdeep=pad.namespace('a').namespace('b');deep.set('key','val');pad.get('a:b:key');// 'val'// Namespace-scoped operationsmemory.keys();// ['fact'] (prefix stripped)memory.clear();// removes only memory:* entries

Parameters:

  • name (string) -- The namespace prefix.

Returns:Scratchpad -- A namespace-scoped scratchpad instance with the same full API.

snapshot()

Captures the full scratchpad state at the current point in time. The returned snapshot is a plain object suitable for serialization.

constsnap=pad.snapshot();// { entries: { ... }, timestamp: 1710000000000, version: 1 }

Returns:ScratchpadSnapshot

restore(snapshot)

Replaces the entire scratchpad state with the contents of a snapshot. Clears all existing entries before restoring. Throws ScratchpadVersionError if the snapshot version is not supported.

pad.restore(snap);

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot previously obtained from snapshot() or serialize().

Returns:void

Throws:ScratchpadVersionError if snapshot.version is not 1.

serialize()

Alias for snapshot(). Returns the same ScratchpadSnapshot structure.

constdata=pad.serialize();

Returns:ScratchpadSnapshot

toContext(options?)

Renders scratchpad contents as a formatted string suitable for injection into an LLM prompt. Supports filtering by tags or namespace, multiple output formats, token budget limits, and custom headers.

pad.set('name','Alice');pad.set('role','admin');pad.toContext();// 'name: Alice\nrole: admin'pad.toContext({format: 'markdown'});// '## name\nAlice\n\n## role\nadmin'pad.toContext({format: 'json'});// '{"name":"Alice","role":"admin"}'pad.toContext({format: 'xml'});// '<entry key="name">Alice</entry>\n<entry key="role">admin</entry>'

Parameters:

OptionTypeDefaultDescription
format'kv' | 'markdown' | 'xml' | 'json''kv'Output format.
filterTagsstring[]undefinedOnly include entries that have at least one of the specified tags.
filterNamespacestringundefinedOnly include entries whose key starts with the given namespace prefix.
maxTokensnumberundefinedTruncate output to fit within this token budget.
tokenCounter(text: string) => numbertext.lengthFunction to count tokens. Used with maxTokens.
includeMetadatabooleanundefinedReserved for future use.
headerstringundefinedText prepended to the output before the formatted entries.

Returns:string

stats()

Returns aggregate statistics about the scratchpad's current state.

constst=pad.stats();// {// size: 3, // live (non-expired) entry count// rawSize: 4, // total entries including expired-not-yet-swept// namespaceCount: 2,// namespaces: ['ctx', 'mem'],// entriesWithTtl: 1,// tagCounts: { geo: 2, important: 1 },// oldestEntryAt: 1710000000000,// newestEntryAt: 1710000001000,// }

Returns:ScratchpadStats

FieldTypeDescription
sizenumberCount of non-expired entries.
rawSizenumberTotal entries in the store, including expired entries not yet swept.
namespaceCountnumberNumber of distinct namespace prefixes.
namespacesstring[]List of distinct namespace prefixes.
entriesWithTtlnumberCount of entries that have a TTL set.
tagCountsRecord<string, number>Count of entries per tag.
oldestEntryAtnumber | nullcreatedAt of the oldest live entry, or null if empty.
newestEntryAtnumber | nullcreatedAt of the newest live entry, or null if empty.

on(event, handler)

Registers an event handler. Returns an unsubscribe function.

constunsub=pad.on('set',({ key, entry, isUpdate })=>{console.log(isUpdate ? 'updated' : 'created',key);});pad.on('delete',({ key, entry })=>{console.log('deleted',key);});pad.on('expire',({ key, entry })=>{console.log('expired',key);});pad.on('clear',({ count })=>{console.log('cleared',count,'entries');});// Stop listeningunsub();

Parameters:

  • event (ScratchpadEventName) -- One of 'set', 'delete', 'expire', 'clear'.
  • handler (ScratchpadEventHandler<K>) -- Callback receiving the event payload.

Event Payloads:

EventPayload
set{ key: string; entry: ScratchpadEntry; isUpdate: boolean }
delete{ key: string; entry: ScratchpadEntry }
expire{ key: string; entry: ScratchpadEntry }
clear{ count: number }

Returns:() => void -- Call to unsubscribe.

save()

Persists the current scratchpad state using the configured PersistenceAdapter. No-op if no adapter was provided.

awaitpad.save();

Returns:Promise<void>

load()

Loads scratchpad state from the configured PersistenceAdapter and restores it. No-op if no adapter was provided or the adapter returns null.

awaitpad.load();

Returns:Promise<void>

destroy()

Cleans up resources. Stops the background sweep timer if one is running.

awaitpad.destroy();

Returns:Promise<void>


TTL Utility Functions

isExpired(entry, now)

Determines whether a scratchpad entry has expired based on its TTL configuration.

import{isExpired}from'agent-scratchpad';constexpired=isExpired(entry,Date.now());

Parameters:

  • entry (ScratchpadEntry) -- The entry to check.
  • now (number) -- Current timestamp in milliseconds.

Returns:boolean -- true if the entry's TTL has elapsed.

Logic:

  • Returns false if entry.ttl is null.
  • For fixed TTL (slidingTtl: false): expired when now >= entry.createdAt + entry.ttl.
  • For sliding TTL (slidingTtl: true): expired when now >= entry.accessedAt + entry.ttl.

expiresAt(entry)

Calculates the absolute expiration timestamp for an entry.

import{expiresAt}from'agent-scratchpad';constexpiry=expiresAt(entry);// number | null

Parameters:

  • entry (ScratchpadEntry) -- The entry to inspect.

Returns:number | null -- The Unix timestamp (ms) when the entry expires, or null if it has no TTL.


Types

ScratchpadEntry<T>

interfaceScratchpadEntry<T=unknown>{key: string;value: T;createdAt: number;// Unix ms when first createdupdatedAt: number;// Unix ms when value last updatedaccessedAt: number;// Unix ms when last read via get()ttl: number|null;// TTL in ms, null = no expirationslidingTtl: boolean;// true = TTL resets on accesstags: string[];// string labels for categorization}

EntryOptions

interfaceEntryOptions{ttl?: number|null;slidingTtl?: boolean;tags?: string[];}

ScratchpadOptions

interfaceScratchpadOptions{defaultTtl?: number|null;defaultSlidingTtl?: boolean;sweepIntervalMs?: number|null;now?: ()=>number;persistence?: PersistenceAdapter;}

ScratchpadSnapshot

interfaceScratchpadSnapshot{entries: Record<string,ScratchpadEntry>;timestamp: number;version: 1;}

ScratchpadStats

interfaceScratchpadStats{size: number;rawSize: number;namespaceCount: number;namespaces: string[];entriesWithTtl: number;tagCounts: Record<string,number>;oldestEntryAt: number|null;newestEntryAt: number|null;}

ToContextOptions

interfaceToContextOptions{format?: 'markdown'|'xml'|'json'|'kv';filterTags?: string[];filterNamespace?: string;maxTokens?: number;tokenCounter?: (text: string)=>number;includeMetadata?: boolean;header?: string;}

PersistenceAdapter

interfacePersistenceAdapter{load(): Promise<ScratchpadSnapshot|null>;save(snap: ScratchpadSnapshot): Promise<void>;}

ScratchpadEvents

interfaceScratchpadEvents{set: {key: string;entry: ScratchpadEntry;isUpdate: boolean};delete: {key: string;entry: ScratchpadEntry};expire: {key: string;entry: ScratchpadEntry};clear: {count: number};}

ScratchpadEventName

typeScratchpadEventName='set'|'delete'|'expire'|'clear';

ScratchpadEventHandler<K>

typeScratchpadEventHandler<KextendsScratchpadEventName>=(data: ScratchpadEvents[K])=>void;

Error Handling

agent-scratchpad exports three error classes, all extending a common base.

ScratchpadError

Base class for all scratchpad errors. Extends Error with a code property.

import{ScratchpadError}from'agent-scratchpad';try{pad.restore(badSnapshot);}catch(err){if(errinstanceofScratchpadError){console.error(err.code);// e.g. 'SCRATCHPAD_VERSION_ERROR'console.error(err.message);// human-readable description}}
PropertyTypeDescription
codestringMachine-readable error code.
messagestringHuman-readable error description.
namestringAlways 'ScratchpadError'.

ScratchpadConfigError

Thrown when invalid configuration is provided to createScratchpad().

  • Code:SCRATCHPAD_CONFIG_ERROR

ScratchpadVersionError

Thrown when restore() encounters a snapshot with an unsupported version number.

  • Code:SCRATCHPAD_VERSION_ERROR
  • Additional property:version (number) -- The unsupported version that was encountered.
import{ScratchpadVersionError}from'agent-scratchpad';try{pad.restore(snap);}catch(err){if(errinstanceofScratchpadVersionError){console.error(`Unsupported version: ${err.version}`);}}

Advanced Usage

Agent Working Memory in a ReAct Loop

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 300_000});// 5-minute default// Step 1: Store tool outputpad.set('search:result',apiResponse,{tags: ['tool-result','search']});// Step 2: Extract and store entitiespad.set('entities:user',{name: 'Alice',id: 42},{tags: ['entity']});pad.set('entities:order',{orderId: '#12345'},{tags: ['entity']});// Step 3: Inject scratchpad into promptconstagentContext=pad.toContext({format: 'markdown',header: '## Agent Working Memory',filterTags: ['entity'],});// Produces:// ## Agent Working Memory// ## entities:user// [object Object]// ...

Namespace Isolation for Multi-Agent Systems

constpad=createScratchpad();constagent1=pad.namespace('agent1');constagent2=pad.namespace('agent2');agent1.set('plan','Research the topic');agent2.set('plan','Draft the response');// Each agent sees only its own entriesagent1.keys();// ['plan']agent2.keys();// ['plan']// Parent sees all entries with prefixed keyspad.keys();// ['agent1:plan', 'agent2:plan']// Clear one agent without affecting the otheragent1.clear();agent2.keys();// ['plan'] -- unaffected

Sliding TTL for Session-Like Data

constpad=createScratchpad();// Session token stays alive as long as the agent keeps accessing itpad.set('session',{token: 'abc123'},{ttl: 30_000,slidingTtl: true});// Each access resets the 30-second expiration windowpad.get('session');// resets timerpad.get('session');// resets timer again// If 30 seconds pass without access, the entry expires

Background Sweep with Event Logging

constpad=createScratchpad({sweepIntervalMs: 10_000});pad.on('expire',({ key, entry })=>{console.log(`Expired: ${key} (created ${newDate(entry.createdAt).toISOString()})`);});pad.set('temp','data',{ttl: 15_000});// The sweep timer runs every 10 seconds and removes expired entries.// The expire event fires for each entry removed by the sweep.// Stop the sweep timer when doneawaitpad.destroy();

Persistence with a File-Based Adapter

import{createScratchpad,PersistenceAdapter,ScratchpadSnapshot}from'agent-scratchpad';importfsfrom'fs/promises';constfileAdapter: PersistenceAdapter={asyncload(){try{constdata=awaitfs.readFile('scratchpad.json','utf8');returnJSON.parse(data)asScratchpadSnapshot;}catch{returnnull;}},asyncsave(snap){awaitfs.writeFile('scratchpad.json',JSON.stringify(snap,null,2));},};constpad=createScratchpad({persistence: fileAdapter});// Restore previous state on startupawaitpad.load();// Work with the scratchpadpad.set('progress','step-3');// Persist state before shutdownawaitpad.save();

Snapshot-Based Backtracking

constpad=createScratchpad();pad.set('approach','strategy-A');pad.set('findings',['result-1']);// Save state before trying something riskyconstcheckpoint=pad.snapshot();pad.set('approach','strategy-B');pad.set('findings',['result-1','result-2-failed']);// Strategy B failed -- roll backpad.restore(checkpoint);pad.get('approach');// 'strategy-A'

Token-Budget-Aware Context Rendering

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();pad.set('summary','A long summary of findings...');pad.set('details','Extensive details that may not fit...');// Use a custom token counter (e.g., tiktoken)constcontext=pad.toContext({format: 'kv',maxTokens: 500,tokenCounter: (text)=>Math.ceil(text.length/4),// rough estimateheader: '## Working Memory',});

Deterministic Testing with Custom Time

import{createScratchpad}from'agent-scratchpad';letnow=0;constpad=createScratchpad({now: ()=>now});pad.set('key','value',{ttl: 100});now=50;pad.get('key');// 'value' -- still alivenow=100;pad.get('key');// undefined -- expired exactly at 100ms

TypeScript

agent-scratchpad is written in TypeScript and ships with full type declarations. All exported functions, interfaces, and types are available for import:

import{createScratchpad,fromSnapshot,toContext,isExpired,expiresAt,ScratchpadError,ScratchpadConfigError,ScratchpadVersionError,}from'agent-scratchpad';importtype{Scratchpad,ScratchpadEntry,ScratchpadOptions,ScratchpadSnapshot,ScratchpadStats,ToContextOptions,EntryOptions,ScratchpadEventName,ScratchpadEventHandler,ScratchpadEvents,PersistenceAdapter,}from'agent-scratchpad';

Generic type parameters on get<T>() and set<T>() provide type-safe value access without casts:

interfaceUser{id: number;name: string;}pad.set<User>('user',{id: 1,name: 'Alice'});constuser=pad.get<User>('user');// user is User | undefined

License

MIT

About

Lightweight key-value scratchpad for agent reasoning

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SiluPanda/agent-scratchpad: Lightweight key-value scratchpad for agent reasoning · GitHub
Skip to content

Repository files navigation

agent-scratchpad

Lightweight key-value scratchpad for AI agent working memory.

npm versionnpm downloadslicensenodeTypeScript

agent-scratchpad is a zero-dependency, in-process key-value store purpose-built for AI agent reasoning loops. Agents executing multi-step workflows (ReAct, Plan-and-Execute, Chain-of-Thought with tool use) need a place to write down intermediate state between steps -- tool outputs, extracted entities, partial computations, decision rationale, and task decomposition state. This package provides that working memory with typed entries, automatic TTL-based expiration, hierarchical namespaces, tag-based querying, point-in-time snapshots, event-driven change observation, pluggable persistence, and a toContext() method that renders scratchpad contents directly into LLM prompts. It works with any agent framework or custom agent loop.

Installation

npm install agent-scratchpad

Quick Start

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();// Store intermediate resultspad.set('step','analyze');pad.set('user',{id: 42,name: 'Alice'});console.log(pad.get('step'));// 'analyze'console.log(pad.has('user'));// trueconsole.log(pad.keys());// ['step', 'user']// Render contents for an LLM promptconstcontext=pad.toContext({format: 'markdown'});

Features

  • Zero runtime dependencies -- all logic uses built-in JavaScript APIs
  • TypeScript-first -- full generic type safety on get<T>() and set<T>()
  • TTL expiration -- fixed or sliding time-to-live with lazy and active sweep modes
  • Hierarchical namespaces -- scope entries per agent, task, or step with pad.namespace('name')
  • Tag-based querying -- label entries and retrieve them with findByTag()
  • Snapshots -- capture and restore full scratchpad state for backtracking and debugging
  • Context rendering -- format entries as Markdown, XML, JSON, or key-value pairs for LLM prompts
  • Event system -- observe set, delete, expire, and clear events
  • Pluggable persistence -- save and load scratchpad state via a simple adapter interface
  • Framework-agnostic -- works with LangChain, Vercel AI SDK, AutoGen, CrewAI, or any custom loop

API Reference

createScratchpad(options?)

Creates a new Scratchpad instance.

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 60_000,defaultSlidingTtl: false,sweepIntervalMs: 10_000,now: ()=>Date.now(),persistence: adapter,});

Parameters:

OptionTypeDefaultDescription
defaultTtlnumber | nullnullDefault TTL in milliseconds applied to entries that do not specify their own. null means no expiration.
defaultSlidingTtlbooleanfalseWhether the default TTL mode is sliding (resets on access) or fixed (from creation).
sweepIntervalMsnumber | nullnullInterval in milliseconds for proactive background sweep of expired entries. null disables active sweep.
now() => numberDate.nowCustom time source. Useful for deterministic testing.
persistencePersistenceAdapterundefinedOptional adapter for saving and loading scratchpad state.

Returns:Scratchpad


fromSnapshot(snapshot, options?)

Creates a new Scratchpad pre-populated from a previously captured snapshot.

import{fromSnapshot}from'agent-scratchpad';constpad=fromSnapshot(snap,{defaultTtl: 30_000});

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot object previously obtained from pad.snapshot() or pad.serialize().
  • options (ScratchpadOptions, optional) -- Configuration options passed to the underlying createScratchpad() call.

Returns:Scratchpad


Scratchpad Methods

set<T>(key, value, options?)

Stores a value under the given key. If the key already exists, updates the value and updatedAt timestamp while preserving createdAt.

pad.set('result',{score: 0.95});pad.set('cache','value',{ttl: 5_000,tags: ['temporary']});pad.set('session',token,{ttl: 30_000,slidingTtl: true});

Parameters:

  • key (string) -- The entry key.
  • value (T) -- The value to store.
  • options (EntryOptions, optional) -- Per-entry configuration.
OptionTypeDefaultDescription
ttlnumber | nullInherits defaultTtlTTL in milliseconds. null disables expiration for this entry.
slidingTtlbooleanInherits defaultSlidingTtlWhether TTL resets on each get() access.
tagsstring[][]String labels for categorizing the entry.

Returns:void

get<T>(key)

Retrieves the value for a key. If the entry has expired, it is removed, an expire event fires, and undefined is returned. On a successful read, accessedAt is updated (which resets the sliding TTL window if applicable).

constuser=pad.get<{id: number;name: string}>('user');

Parameters:

  • key (string) -- The entry key.

Returns:T | undefined

has(key)

Checks whether a key exists and is not expired. Expired entries are removed and trigger an expire event.

if(pad.has('apiResponse')){// entry is live}

Parameters:

  • key (string) -- The entry key.

Returns:boolean

delete(key)

Removes an entry by key. Fires a delete event if the entry existed.

constremoved=pad.delete('staleData');// true if it existed

Parameters:

  • key (string) -- The entry key.

Returns:boolean -- true if the entry existed and was removed, false otherwise.

clear()

Removes all entries from the scratchpad. Fires a clear event with the count of removed entries.

pad.clear();

Returns:void

keys()

Returns an array of all non-expired keys. Expired entries encountered during iteration are excluded.

constallKeys=pad.keys();// ['step', 'user', 'result']

Returns:string[]

entries()

Returns an array of [key, ScratchpadEntry] tuples for all non-expired entries.

for(const[key,entry]ofpad.entries()){console.log(key,entry.value,entry.tags);}

Returns:[string, ScratchpadEntry][]

findByTag(tag)

Returns all non-expired entries whose tags array includes the given tag (exact match).

pad.set('london','UK capital',{tags: ['geo','important']});pad.set('paris','France capital',{tags: ['geo']});constgeoEntries=pad.findByTag('geo');// both entries

Parameters:

  • tag (string) -- The tag to search for.

Returns:ScratchpadEntry[]

namespace(name)

Returns a scoped view of the scratchpad where all operations are prefixed with name:. Namespaces share the underlying storage with the parent -- they are views, not copies. Namespaces can be nested.

constmemory=pad.namespace('memory');memory.set('fact','The sky is blue');memory.get('fact');// 'The sky is blue'pad.get('memory:fact');// 'The sky is blue'// Nested namespaces compose prefixesconstdeep=pad.namespace('a').namespace('b');deep.set('key','val');pad.get('a:b:key');// 'val'// Namespace-scoped operationsmemory.keys();// ['fact'] (prefix stripped)memory.clear();// removes only memory:* entries

Parameters:

  • name (string) -- The namespace prefix.

Returns:Scratchpad -- A namespace-scoped scratchpad instance with the same full API.

snapshot()

Captures the full scratchpad state at the current point in time. The returned snapshot is a plain object suitable for serialization.

constsnap=pad.snapshot();// { entries: { ... }, timestamp: 1710000000000, version: 1 }

Returns:ScratchpadSnapshot

restore(snapshot)

Replaces the entire scratchpad state with the contents of a snapshot. Clears all existing entries before restoring. Throws ScratchpadVersionError if the snapshot version is not supported.

pad.restore(snap);

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot previously obtained from snapshot() or serialize().

Returns:void

Throws:ScratchpadVersionError if snapshot.version is not 1.

serialize()

Alias for snapshot(). Returns the same ScratchpadSnapshot structure.

constdata=pad.serialize();

Returns:ScratchpadSnapshot

toContext(options?)

Renders scratchpad contents as a formatted string suitable for injection into an LLM prompt. Supports filtering by tags or namespace, multiple output formats, token budget limits, and custom headers.

pad.set('name','Alice');pad.set('role','admin');pad.toContext();// 'name: Alice\nrole: admin'pad.toContext({format: 'markdown'});// '## name\nAlice\n\n## role\nadmin'pad.toContext({format: 'json'});// '{"name":"Alice","role":"admin"}'pad.toContext({format: 'xml'});// '<entry key="name">Alice</entry>\n<entry key="role">admin</entry>'

Parameters:

OptionTypeDefaultDescription
format'kv' | 'markdown' | 'xml' | 'json''kv'Output format.
filterTagsstring[]undefinedOnly include entries that have at least one of the specified tags.
filterNamespacestringundefinedOnly include entries whose key starts with the given namespace prefix.
maxTokensnumberundefinedTruncate output to fit within this token budget.
tokenCounter(text: string) => numbertext.lengthFunction to count tokens. Used with maxTokens.
includeMetadatabooleanundefinedReserved for future use.
headerstringundefinedText prepended to the output before the formatted entries.

Returns:string

stats()

Returns aggregate statistics about the scratchpad's current state.

constst=pad.stats();// {// size: 3, // live (non-expired) entry count// rawSize: 4, // total entries including expired-not-yet-swept// namespaceCount: 2,// namespaces: ['ctx', 'mem'],// entriesWithTtl: 1,// tagCounts: { geo: 2, important: 1 },// oldestEntryAt: 1710000000000,// newestEntryAt: 1710000001000,// }

Returns:ScratchpadStats

FieldTypeDescription
sizenumberCount of non-expired entries.
rawSizenumberTotal entries in the store, including expired entries not yet swept.
namespaceCountnumberNumber of distinct namespace prefixes.
namespacesstring[]List of distinct namespace prefixes.
entriesWithTtlnumberCount of entries that have a TTL set.
tagCountsRecord<string, number>Count of entries per tag.
oldestEntryAtnumber | nullcreatedAt of the oldest live entry, or null if empty.
newestEntryAtnumber | nullcreatedAt of the newest live entry, or null if empty.

on(event, handler)

Registers an event handler. Returns an unsubscribe function.

constunsub=pad.on('set',({ key, entry, isUpdate })=>{console.log(isUpdate ? 'updated' : 'created',key);});pad.on('delete',({ key, entry })=>{console.log('deleted',key);});pad.on('expire',({ key, entry })=>{console.log('expired',key);});pad.on('clear',({ count })=>{console.log('cleared',count,'entries');});// Stop listeningunsub();

Parameters:

  • event (ScratchpadEventName) -- One of 'set', 'delete', 'expire', 'clear'.
  • handler (ScratchpadEventHandler<K>) -- Callback receiving the event payload.

Event Payloads:

EventPayload
set{ key: string; entry: ScratchpadEntry; isUpdate: boolean }
delete{ key: string; entry: ScratchpadEntry }
expire{ key: string; entry: ScratchpadEntry }
clear{ count: number }

Returns:() => void -- Call to unsubscribe.

save()

Persists the current scratchpad state using the configured PersistenceAdapter. No-op if no adapter was provided.

awaitpad.save();

Returns:Promise<void>

load()

Loads scratchpad state from the configured PersistenceAdapter and restores it. No-op if no adapter was provided or the adapter returns null.

awaitpad.load();

Returns:Promise<void>

destroy()

Cleans up resources. Stops the background sweep timer if one is running.

awaitpad.destroy();

Returns:Promise<void>


TTL Utility Functions

isExpired(entry, now)

Determines whether a scratchpad entry has expired based on its TTL configuration.

import{isExpired}from'agent-scratchpad';constexpired=isExpired(entry,Date.now());

Parameters:

  • entry (ScratchpadEntry) -- The entry to check.
  • now (number) -- Current timestamp in milliseconds.

Returns:boolean -- true if the entry's TTL has elapsed.

Logic:

  • Returns false if entry.ttl is null.
  • For fixed TTL (slidingTtl: false): expired when now >= entry.createdAt + entry.ttl.
  • For sliding TTL (slidingTtl: true): expired when now >= entry.accessedAt + entry.ttl.

expiresAt(entry)

Calculates the absolute expiration timestamp for an entry.

import{expiresAt}from'agent-scratchpad';constexpiry=expiresAt(entry);// number | null

Parameters:

  • entry (ScratchpadEntry) -- The entry to inspect.

Returns:number | null -- The Unix timestamp (ms) when the entry expires, or null if it has no TTL.


Types

ScratchpadEntry<T>

interfaceScratchpadEntry<T=unknown>{key: string;value: T;createdAt: number;// Unix ms when first createdupdatedAt: number;// Unix ms when value last updatedaccessedAt: number;// Unix ms when last read via get()ttl: number|null;// TTL in ms, null = no expirationslidingTtl: boolean;// true = TTL resets on accesstags: string[];// string labels for categorization}

EntryOptions

interfaceEntryOptions{ttl?: number|null;slidingTtl?: boolean;tags?: string[];}

ScratchpadOptions

interfaceScratchpadOptions{defaultTtl?: number|null;defaultSlidingTtl?: boolean;sweepIntervalMs?: number|null;now?: ()=>number;persistence?: PersistenceAdapter;}

ScratchpadSnapshot

interfaceScratchpadSnapshot{entries: Record<string,ScratchpadEntry>;timestamp: number;version: 1;}

ScratchpadStats

interfaceScratchpadStats{size: number;rawSize: number;namespaceCount: number;namespaces: string[];entriesWithTtl: number;tagCounts: Record<string,number>;oldestEntryAt: number|null;newestEntryAt: number|null;}

ToContextOptions

interfaceToContextOptions{format?: 'markdown'|'xml'|'json'|'kv';filterTags?: string[];filterNamespace?: string;maxTokens?: number;tokenCounter?: (text: string)=>number;includeMetadata?: boolean;header?: string;}

PersistenceAdapter

interfacePersistenceAdapter{load(): Promise<ScratchpadSnapshot|null>;save(snap: ScratchpadSnapshot): Promise<void>;}

ScratchpadEvents

interfaceScratchpadEvents{set: {key: string;entry: ScratchpadEntry;isUpdate: boolean};delete: {key: string;entry: ScratchpadEntry};expire: {key: string;entry: ScratchpadEntry};clear: {count: number};}

ScratchpadEventName

typeScratchpadEventName='set'|'delete'|'expire'|'clear';

ScratchpadEventHandler<K>

typeScratchpadEventHandler<KextendsScratchpadEventName>=(data: ScratchpadEvents[K])=>void;

Error Handling

agent-scratchpad exports three error classes, all extending a common base.

ScratchpadError

Base class for all scratchpad errors. Extends Error with a code property.

import{ScratchpadError}from'agent-scratchpad';try{pad.restore(badSnapshot);}catch(err){if(errinstanceofScratchpadError){console.error(err.code);// e.g. 'SCRATCHPAD_VERSION_ERROR'console.error(err.message);// human-readable description}}
PropertyTypeDescription
codestringMachine-readable error code.
messagestringHuman-readable error description.
namestringAlways 'ScratchpadError'.

ScratchpadConfigError

Thrown when invalid configuration is provided to createScratchpad().

  • Code:SCRATCHPAD_CONFIG_ERROR

ScratchpadVersionError

Thrown when restore() encounters a snapshot with an unsupported version number.

  • Code:SCRATCHPAD_VERSION_ERROR
  • Additional property:version (number) -- The unsupported version that was encountered.
import{ScratchpadVersionError}from'agent-scratchpad';try{pad.restore(snap);}catch(err){if(errinstanceofScratchpadVersionError){console.error(`Unsupported version: ${err.version}`);}}

Advanced Usage

Agent Working Memory in a ReAct Loop

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 300_000});// 5-minute default// Step 1: Store tool outputpad.set('search:result',apiResponse,{tags: ['tool-result','search']});// Step 2: Extract and store entitiespad.set('entities:user',{name: 'Alice',id: 42},{tags: ['entity']});pad.set('entities:order',{orderId: '#12345'},{tags: ['entity']});// Step 3: Inject scratchpad into promptconstagentContext=pad.toContext({format: 'markdown',header: '## Agent Working Memory',filterTags: ['entity'],});// Produces:// ## Agent Working Memory// ## entities:user// [object Object]// ...

Namespace Isolation for Multi-Agent Systems

constpad=createScratchpad();constagent1=pad.namespace('agent1');constagent2=pad.namespace('agent2');agent1.set('plan','Research the topic');agent2.set('plan','Draft the response');// Each agent sees only its own entriesagent1.keys();// ['plan']agent2.keys();// ['plan']// Parent sees all entries with prefixed keyspad.keys();// ['agent1:plan', 'agent2:plan']// Clear one agent without affecting the otheragent1.clear();agent2.keys();// ['plan'] -- unaffected

Sliding TTL for Session-Like Data

constpad=createScratchpad();// Session token stays alive as long as the agent keeps accessing itpad.set('session',{token: 'abc123'},{ttl: 30_000,slidingTtl: true});// Each access resets the 30-second expiration windowpad.get('session');// resets timerpad.get('session');// resets timer again// If 30 seconds pass without access, the entry expires

Background Sweep with Event Logging

constpad=createScratchpad({sweepIntervalMs: 10_000});pad.on('expire',({ key, entry })=>{console.log(`Expired: ${key} (created ${newDate(entry.createdAt).toISOString()})`);});pad.set('temp','data',{ttl: 15_000});// The sweep timer runs every 10 seconds and removes expired entries.// The expire event fires for each entry removed by the sweep.// Stop the sweep timer when doneawaitpad.destroy();

Persistence with a File-Based Adapter

import{createScratchpad,PersistenceAdapter,ScratchpadSnapshot}from'agent-scratchpad';importfsfrom'fs/promises';constfileAdapter: PersistenceAdapter={asyncload(){try{constdata=awaitfs.readFile('scratchpad.json','utf8');returnJSON.parse(data)asScratchpadSnapshot;}catch{returnnull;}},asyncsave(snap){awaitfs.writeFile('scratchpad.json',JSON.stringify(snap,null,2));},};constpad=createScratchpad({persistence: fileAdapter});// Restore previous state on startupawaitpad.load();// Work with the scratchpadpad.set('progress','step-3');// Persist state before shutdownawaitpad.save();

Snapshot-Based Backtracking

constpad=createScratchpad();pad.set('approach','strategy-A');pad.set('findings',['result-1']);// Save state before trying something riskyconstcheckpoint=pad.snapshot();pad.set('approach','strategy-B');pad.set('findings',['result-1','result-2-failed']);// Strategy B failed -- roll backpad.restore(checkpoint);pad.get('approach');// 'strategy-A'

Token-Budget-Aware Context Rendering

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();pad.set('summary','A long summary of findings...');pad.set('details','Extensive details that may not fit...');// Use a custom token counter (e.g., tiktoken)constcontext=pad.toContext({format: 'kv',maxTokens: 500,tokenCounter: (text)=>Math.ceil(text.length/4),// rough estimateheader: '## Working Memory',});

Deterministic Testing with Custom Time

import{createScratchpad}from'agent-scratchpad';letnow=0;constpad=createScratchpad({now: ()=>now});pad.set('key','value',{ttl: 100});now=50;pad.get('key');// 'value' -- still alivenow=100;pad.get('key');// undefined -- expired exactly at 100ms

TypeScript

agent-scratchpad is written in TypeScript and ships with full type declarations. All exported functions, interfaces, and types are available for import:

import{createScratchpad,fromSnapshot,toContext,isExpired,expiresAt,ScratchpadError,ScratchpadConfigError,ScratchpadVersionError,}from'agent-scratchpad';importtype{Scratchpad,ScratchpadEntry,ScratchpadOptions,ScratchpadSnapshot,ScratchpadStats,ToContextOptions,EntryOptions,ScratchpadEventName,ScratchpadEventHandler,ScratchpadEvents,PersistenceAdapter,}from'agent-scratchpad';

Generic type parameters on get<T>() and set<T>() provide type-safe value access without casts:

interfaceUser{id: number;name: string;}pad.set<User>('user',{id: 1,name: 'Alice'});constuser=pad.get<User>('user');// user is User | undefined

License

MIT

About

Lightweight key-value scratchpad for agent reasoning

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - SiluPanda/agent-scratchpad: Lightweight key-value scratchpad for agent reasoning · GitHub
Skip to content

Repository files navigation

agent-scratchpad

Lightweight key-value scratchpad for AI agent working memory.

npm versionnpm downloadslicensenodeTypeScript

agent-scratchpad is a zero-dependency, in-process key-value store purpose-built for AI agent reasoning loops. Agents executing multi-step workflows (ReAct, Plan-and-Execute, Chain-of-Thought with tool use) need a place to write down intermediate state between steps -- tool outputs, extracted entities, partial computations, decision rationale, and task decomposition state. This package provides that working memory with typed entries, automatic TTL-based expiration, hierarchical namespaces, tag-based querying, point-in-time snapshots, event-driven change observation, pluggable persistence, and a toContext() method that renders scratchpad contents directly into LLM prompts. It works with any agent framework or custom agent loop.

Installation

npm install agent-scratchpad

Quick Start

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();// Store intermediate resultspad.set('step','analyze');pad.set('user',{id: 42,name: 'Alice'});console.log(pad.get('step'));// 'analyze'console.log(pad.has('user'));// trueconsole.log(pad.keys());// ['step', 'user']// Render contents for an LLM promptconstcontext=pad.toContext({format: 'markdown'});

Features

  • Zero runtime dependencies -- all logic uses built-in JavaScript APIs
  • TypeScript-first -- full generic type safety on get<T>() and set<T>()
  • TTL expiration -- fixed or sliding time-to-live with lazy and active sweep modes
  • Hierarchical namespaces -- scope entries per agent, task, or step with pad.namespace('name')
  • Tag-based querying -- label entries and retrieve them with findByTag()
  • Snapshots -- capture and restore full scratchpad state for backtracking and debugging
  • Context rendering -- format entries as Markdown, XML, JSON, or key-value pairs for LLM prompts
  • Event system -- observe set, delete, expire, and clear events
  • Pluggable persistence -- save and load scratchpad state via a simple adapter interface
  • Framework-agnostic -- works with LangChain, Vercel AI SDK, AutoGen, CrewAI, or any custom loop

API Reference

createScratchpad(options?)

Creates a new Scratchpad instance.

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 60_000,defaultSlidingTtl: false,sweepIntervalMs: 10_000,now: ()=>Date.now(),persistence: adapter,});

Parameters:

OptionTypeDefaultDescription
defaultTtlnumber | nullnullDefault TTL in milliseconds applied to entries that do not specify their own. null means no expiration.
defaultSlidingTtlbooleanfalseWhether the default TTL mode is sliding (resets on access) or fixed (from creation).
sweepIntervalMsnumber | nullnullInterval in milliseconds for proactive background sweep of expired entries. null disables active sweep.
now() => numberDate.nowCustom time source. Useful for deterministic testing.
persistencePersistenceAdapterundefinedOptional adapter for saving and loading scratchpad state.

Returns:Scratchpad


fromSnapshot(snapshot, options?)

Creates a new Scratchpad pre-populated from a previously captured snapshot.

import{fromSnapshot}from'agent-scratchpad';constpad=fromSnapshot(snap,{defaultTtl: 30_000});

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot object previously obtained from pad.snapshot() or pad.serialize().
  • options (ScratchpadOptions, optional) -- Configuration options passed to the underlying createScratchpad() call.

Returns:Scratchpad


Scratchpad Methods

set<T>(key, value, options?)

Stores a value under the given key. If the key already exists, updates the value and updatedAt timestamp while preserving createdAt.

pad.set('result',{score: 0.95});pad.set('cache','value',{ttl: 5_000,tags: ['temporary']});pad.set('session',token,{ttl: 30_000,slidingTtl: true});

Parameters:

  • key (string) -- The entry key.
  • value (T) -- The value to store.
  • options (EntryOptions, optional) -- Per-entry configuration.
OptionTypeDefaultDescription
ttlnumber | nullInherits defaultTtlTTL in milliseconds. null disables expiration for this entry.
slidingTtlbooleanInherits defaultSlidingTtlWhether TTL resets on each get() access.
tagsstring[][]String labels for categorizing the entry.

Returns:void

get<T>(key)

Retrieves the value for a key. If the entry has expired, it is removed, an expire event fires, and undefined is returned. On a successful read, accessedAt is updated (which resets the sliding TTL window if applicable).

constuser=pad.get<{id: number;name: string}>('user');

Parameters:

  • key (string) -- The entry key.

Returns:T | undefined

has(key)

Checks whether a key exists and is not expired. Expired entries are removed and trigger an expire event.

if(pad.has('apiResponse')){// entry is live}

Parameters:

  • key (string) -- The entry key.

Returns:boolean

delete(key)

Removes an entry by key. Fires a delete event if the entry existed.

constremoved=pad.delete('staleData');// true if it existed

Parameters:

  • key (string) -- The entry key.

Returns:boolean -- true if the entry existed and was removed, false otherwise.

clear()

Removes all entries from the scratchpad. Fires a clear event with the count of removed entries.

pad.clear();

Returns:void

keys()

Returns an array of all non-expired keys. Expired entries encountered during iteration are excluded.

constallKeys=pad.keys();// ['step', 'user', 'result']

Returns:string[]

entries()

Returns an array of [key, ScratchpadEntry] tuples for all non-expired entries.

for(const[key,entry]ofpad.entries()){console.log(key,entry.value,entry.tags);}

Returns:[string, ScratchpadEntry][]

findByTag(tag)

Returns all non-expired entries whose tags array includes the given tag (exact match).

pad.set('london','UK capital',{tags: ['geo','important']});pad.set('paris','France capital',{tags: ['geo']});constgeoEntries=pad.findByTag('geo');// both entries

Parameters:

  • tag (string) -- The tag to search for.

Returns:ScratchpadEntry[]

namespace(name)

Returns a scoped view of the scratchpad where all operations are prefixed with name:. Namespaces share the underlying storage with the parent -- they are views, not copies. Namespaces can be nested.

constmemory=pad.namespace('memory');memory.set('fact','The sky is blue');memory.get('fact');// 'The sky is blue'pad.get('memory:fact');// 'The sky is blue'// Nested namespaces compose prefixesconstdeep=pad.namespace('a').namespace('b');deep.set('key','val');pad.get('a:b:key');// 'val'// Namespace-scoped operationsmemory.keys();// ['fact'] (prefix stripped)memory.clear();// removes only memory:* entries

Parameters:

  • name (string) -- The namespace prefix.

Returns:Scratchpad -- A namespace-scoped scratchpad instance with the same full API.

snapshot()

Captures the full scratchpad state at the current point in time. The returned snapshot is a plain object suitable for serialization.

constsnap=pad.snapshot();// { entries: { ... }, timestamp: 1710000000000, version: 1 }

Returns:ScratchpadSnapshot

restore(snapshot)

Replaces the entire scratchpad state with the contents of a snapshot. Clears all existing entries before restoring. Throws ScratchpadVersionError if the snapshot version is not supported.

pad.restore(snap);

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot previously obtained from snapshot() or serialize().

Returns:void

Throws:ScratchpadVersionError if snapshot.version is not 1.

serialize()

Alias for snapshot(). Returns the same ScratchpadSnapshot structure.

constdata=pad.serialize();

Returns:ScratchpadSnapshot

toContext(options?)

Renders scratchpad contents as a formatted string suitable for injection into an LLM prompt. Supports filtering by tags or namespace, multiple output formats, token budget limits, and custom headers.

pad.set('name','Alice');pad.set('role','admin');pad.toContext();// 'name: Alice\nrole: admin'pad.toContext({format: 'markdown'});// '## name\nAlice\n\n## role\nadmin'pad.toContext({format: 'json'});// '{"name":"Alice","role":"admin"}'pad.toContext({format: 'xml'});// '<entry key="name">Alice</entry>\n<entry key="role">admin</entry>'

Parameters:

OptionTypeDefaultDescription
format'kv' | 'markdown' | 'xml' | 'json''kv'Output format.
filterTagsstring[]undefinedOnly include entries that have at least one of the specified tags.
filterNamespacestringundefinedOnly include entries whose key starts with the given namespace prefix.
maxTokensnumberundefinedTruncate output to fit within this token budget.
tokenCounter(text: string) => numbertext.lengthFunction to count tokens. Used with maxTokens.
includeMetadatabooleanundefinedReserved for future use.
headerstringundefinedText prepended to the output before the formatted entries.

Returns:string

stats()

Returns aggregate statistics about the scratchpad's current state.

constst=pad.stats();// {// size: 3, // live (non-expired) entry count// rawSize: 4, // total entries including expired-not-yet-swept// namespaceCount: 2,// namespaces: ['ctx', 'mem'],// entriesWithTtl: 1,// tagCounts: { geo: 2, important: 1 },// oldestEntryAt: 1710000000000,// newestEntryAt: 1710000001000,// }

Returns:ScratchpadStats

FieldTypeDescription
sizenumberCount of non-expired entries.
rawSizenumberTotal entries in the store, including expired entries not yet swept.
namespaceCountnumberNumber of distinct namespace prefixes.
namespacesstring[]List of distinct namespace prefixes.
entriesWithTtlnumberCount of entries that have a TTL set.
tagCountsRecord<string, number>Count of entries per tag.
oldestEntryAtnumber | nullcreatedAt of the oldest live entry, or null if empty.
newestEntryAtnumber | nullcreatedAt of the newest live entry, or null if empty.

on(event, handler)

Registers an event handler. Returns an unsubscribe function.

constunsub=pad.on('set',({ key, entry, isUpdate })=>{console.log(isUpdate ? 'updated' : 'created',key);});pad.on('delete',({ key, entry })=>{console.log('deleted',key);});pad.on('expire',({ key, entry })=>{console.log('expired',key);});pad.on('clear',({ count })=>{console.log('cleared',count,'entries');});// Stop listeningunsub();

Parameters:

  • event (ScratchpadEventName) -- One of 'set', 'delete', 'expire', 'clear'.
  • handler (ScratchpadEventHandler<K>) -- Callback receiving the event payload.

Event Payloads:

EventPayload
set{ key: string; entry: ScratchpadEntry; isUpdate: boolean }
delete{ key: string; entry: ScratchpadEntry }
expire{ key: string; entry: ScratchpadEntry }
clear{ count: number }

Returns:() => void -- Call to unsubscribe.

save()

Persists the current scratchpad state using the configured PersistenceAdapter. No-op if no adapter was provided.

awaitpad.save();

Returns:Promise<void>

load()

Loads scratchpad state from the configured PersistenceAdapter and restores it. No-op if no adapter was provided or the adapter returns null.

awaitpad.load();

Returns:Promise<void>

destroy()

Cleans up resources. Stops the background sweep timer if one is running.

awaitpad.destroy();

Returns:Promise<void>


TTL Utility Functions

isExpired(entry, now)

Determines whether a scratchpad entry has expired based on its TTL configuration.

import{isExpired}from'agent-scratchpad';constexpired=isExpired(entry,Date.now());

Parameters:

  • entry (ScratchpadEntry) -- The entry to check.
  • now (number) -- Current timestamp in milliseconds.

Returns:boolean -- true if the entry's TTL has elapsed.

Logic:

  • Returns false if entry.ttl is null.
  • For fixed TTL (slidingTtl: false): expired when now >= entry.createdAt + entry.ttl.
  • For sliding TTL (slidingTtl: true): expired when now >= entry.accessedAt + entry.ttl.

expiresAt(entry)

Calculates the absolute expiration timestamp for an entry.

import{expiresAt}from'agent-scratchpad';constexpiry=expiresAt(entry);// number | null

Parameters:

  • entry (ScratchpadEntry) -- The entry to inspect.

Returns:number | null -- The Unix timestamp (ms) when the entry expires, or null if it has no TTL.


Types

ScratchpadEntry<T>

interfaceScratchpadEntry<T=unknown>{key: string;value: T;createdAt: number;// Unix ms when first createdupdatedAt: number;// Unix ms when value last updatedaccessedAt: number;// Unix ms when last read via get()ttl: number|null;// TTL in ms, null = no expirationslidingTtl: boolean;// true = TTL resets on accesstags: string[];// string labels for categorization}

EntryOptions

interfaceEntryOptions{ttl?: number|null;slidingTtl?: boolean;tags?: string[];}

ScratchpadOptions

interfaceScratchpadOptions{defaultTtl?: number|null;defaultSlidingTtl?: boolean;sweepIntervalMs?: number|null;now?: ()=>number;persistence?: PersistenceAdapter;}

ScratchpadSnapshot

interfaceScratchpadSnapshot{entries: Record<string,ScratchpadEntry>;timestamp: number;version: 1;}

ScratchpadStats

interfaceScratchpadStats{size: number;rawSize: number;namespaceCount: number;namespaces: string[];entriesWithTtl: number;tagCounts: Record<string,number>;oldestEntryAt: number|null;newestEntryAt: number|null;}

ToContextOptions

interfaceToContextOptions{format?: 'markdown'|'xml'|'json'|'kv';filterTags?: string[];filterNamespace?: string;maxTokens?: number;tokenCounter?: (text: string)=>number;includeMetadata?: boolean;header?: string;}

PersistenceAdapter

interfacePersistenceAdapter{load(): Promise<ScratchpadSnapshot|null>;save(snap: ScratchpadSnapshot): Promise<void>;}

ScratchpadEvents

interfaceScratchpadEvents{set: {key: string;entry: ScratchpadEntry;isUpdate: boolean};delete: {key: string;entry: ScratchpadEntry};expire: {key: string;entry: ScratchpadEntry};clear: {count: number};}

ScratchpadEventName

typeScratchpadEventName='set'|'delete'|'expire'|'clear';

ScratchpadEventHandler<K>

typeScratchpadEventHandler<KextendsScratchpadEventName>=(data: ScratchpadEvents[K])=>void;

Error Handling

agent-scratchpad exports three error classes, all extending a common base.

ScratchpadError

Base class for all scratchpad errors. Extends Error with a code property.

import{ScratchpadError}from'agent-scratchpad';try{pad.restore(badSnapshot);}catch(err){if(errinstanceofScratchpadError){console.error(err.code);// e.g. 'SCRATCHPAD_VERSION_ERROR'console.error(err.message);// human-readable description}}
PropertyTypeDescription
codestringMachine-readable error code.
messagestringHuman-readable error description.
namestringAlways 'ScratchpadError'.

ScratchpadConfigError

Thrown when invalid configuration is provided to createScratchpad().

  • Code:SCRATCHPAD_CONFIG_ERROR

ScratchpadVersionError

Thrown when restore() encounters a snapshot with an unsupported version number.

  • Code:SCRATCHPAD_VERSION_ERROR
  • Additional property:version (number) -- The unsupported version that was encountered.
import{ScratchpadVersionError}from'agent-scratchpad';try{pad.restore(snap);}catch(err){if(errinstanceofScratchpadVersionError){console.error(`Unsupported version: ${err.version}`);}}

Advanced Usage

Agent Working Memory in a ReAct Loop

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 300_000});// 5-minute default// Step 1: Store tool outputpad.set('search:result',apiResponse,{tags: ['tool-result','search']});// Step 2: Extract and store entitiespad.set('entities:user',{name: 'Alice',id: 42},{tags: ['entity']});pad.set('entities:order',{orderId: '#12345'},{tags: ['entity']});// Step 3: Inject scratchpad into promptconstagentContext=pad.toContext({format: 'markdown',header: '## Agent Working Memory',filterTags: ['entity'],});// Produces:// ## Agent Working Memory// ## entities:user// [object Object]// ...

Namespace Isolation for Multi-Agent Systems

constpad=createScratchpad();constagent1=pad.namespace('agent1');constagent2=pad.namespace('agent2');agent1.set('plan','Research the topic');agent2.set('plan','Draft the response');// Each agent sees only its own entriesagent1.keys();// ['plan']agent2.keys();// ['plan']// Parent sees all entries with prefixed keyspad.keys();// ['agent1:plan', 'agent2:plan']// Clear one agent without affecting the otheragent1.clear();agent2.keys();// ['plan'] -- unaffected

Sliding TTL for Session-Like Data

constpad=createScratchpad();// Session token stays alive as long as the agent keeps accessing itpad.set('session',{token: 'abc123'},{ttl: 30_000,slidingTtl: true});// Each access resets the 30-second expiration windowpad.get('session');// resets timerpad.get('session');// resets timer again// If 30 seconds pass without access, the entry expires

Background Sweep with Event Logging

constpad=createScratchpad({sweepIntervalMs: 10_000});pad.on('expire',({ key, entry })=>{console.log(`Expired: ${key} (created ${newDate(entry.createdAt).toISOString()})`);});pad.set('temp','data',{ttl: 15_000});// The sweep timer runs every 10 seconds and removes expired entries.// The expire event fires for each entry removed by the sweep.// Stop the sweep timer when doneawaitpad.destroy();

Persistence with a File-Based Adapter

import{createScratchpad,PersistenceAdapter,ScratchpadSnapshot}from'agent-scratchpad';importfsfrom'fs/promises';constfileAdapter: PersistenceAdapter={asyncload(){try{constdata=awaitfs.readFile('scratchpad.json','utf8');returnJSON.parse(data)asScratchpadSnapshot;}catch{returnnull;}},asyncsave(snap){awaitfs.writeFile('scratchpad.json',JSON.stringify(snap,null,2));},};constpad=createScratchpad({persistence: fileAdapter});// Restore previous state on startupawaitpad.load();// Work with the scratchpadpad.set('progress','step-3');// Persist state before shutdownawaitpad.save();

Snapshot-Based Backtracking

constpad=createScratchpad();pad.set('approach','strategy-A');pad.set('findings',['result-1']);// Save state before trying something riskyconstcheckpoint=pad.snapshot();pad.set('approach','strategy-B');pad.set('findings',['result-1','result-2-failed']);// Strategy B failed -- roll backpad.restore(checkpoint);pad.get('approach');// 'strategy-A'

Token-Budget-Aware Context Rendering

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();pad.set('summary','A long summary of findings...');pad.set('details','Extensive details that may not fit...');// Use a custom token counter (e.g., tiktoken)constcontext=pad.toContext({format: 'kv',maxTokens: 500,tokenCounter: (text)=>Math.ceil(text.length/4),// rough estimateheader: '## Working Memory',});

Deterministic Testing with Custom Time

import{createScratchpad}from'agent-scratchpad';letnow=0;constpad=createScratchpad({now: ()=>now});pad.set('key','value',{ttl: 100});now=50;pad.get('key');// 'value' -- still alivenow=100;pad.get('key');// undefined -- expired exactly at 100ms

TypeScript

agent-scratchpad is written in TypeScript and ships with full type declarations. All exported functions, interfaces, and types are available for import:

import{createScratchpad,fromSnapshot,toContext,isExpired,expiresAt,ScratchpadError,ScratchpadConfigError,ScratchpadVersionError,}from'agent-scratchpad';importtype{Scratchpad,ScratchpadEntry,ScratchpadOptions,ScratchpadSnapshot,ScratchpadStats,ToContextOptions,EntryOptions,ScratchpadEventName,ScratchpadEventHandler,ScratchpadEvents,PersistenceAdapter,}from'agent-scratchpad';

Generic type parameters on get<T>() and set<T>() provide type-safe value access without casts:

interfaceUser{id: number;name: string;}pad.set<User>('user',{id: 1,name: 'Alice'});constuser=pad.get<User>('user');// user is User | undefined

License

MIT

About

Lightweight key-value scratchpad for agent reasoning

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SiluPanda/agent-scratchpad: Lightweight key-value scratchpad for agent reasoning · GitHub
Skip to content

Repository files navigation

agent-scratchpad

Lightweight key-value scratchpad for AI agent working memory.

npm versionnpm downloadslicensenodeTypeScript

agent-scratchpad is a zero-dependency, in-process key-value store purpose-built for AI agent reasoning loops. Agents executing multi-step workflows (ReAct, Plan-and-Execute, Chain-of-Thought with tool use) need a place to write down intermediate state between steps -- tool outputs, extracted entities, partial computations, decision rationale, and task decomposition state. This package provides that working memory with typed entries, automatic TTL-based expiration, hierarchical namespaces, tag-based querying, point-in-time snapshots, event-driven change observation, pluggable persistence, and a toContext() method that renders scratchpad contents directly into LLM prompts. It works with any agent framework or custom agent loop.

Installation

npm install agent-scratchpad

Quick Start

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();// Store intermediate resultspad.set('step','analyze');pad.set('user',{id: 42,name: 'Alice'});console.log(pad.get('step'));// 'analyze'console.log(pad.has('user'));// trueconsole.log(pad.keys());// ['step', 'user']// Render contents for an LLM promptconstcontext=pad.toContext({format: 'markdown'});

Features

  • Zero runtime dependencies -- all logic uses built-in JavaScript APIs
  • TypeScript-first -- full generic type safety on get<T>() and set<T>()
  • TTL expiration -- fixed or sliding time-to-live with lazy and active sweep modes
  • Hierarchical namespaces -- scope entries per agent, task, or step with pad.namespace('name')
  • Tag-based querying -- label entries and retrieve them with findByTag()
  • Snapshots -- capture and restore full scratchpad state for backtracking and debugging
  • Context rendering -- format entries as Markdown, XML, JSON, or key-value pairs for LLM prompts
  • Event system -- observe set, delete, expire, and clear events
  • Pluggable persistence -- save and load scratchpad state via a simple adapter interface
  • Framework-agnostic -- works with LangChain, Vercel AI SDK, AutoGen, CrewAI, or any custom loop

API Reference

createScratchpad(options?)

Creates a new Scratchpad instance.

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 60_000,defaultSlidingTtl: false,sweepIntervalMs: 10_000,now: ()=>Date.now(),persistence: adapter,});

Parameters:

OptionTypeDefaultDescription
defaultTtlnumber | nullnullDefault TTL in milliseconds applied to entries that do not specify their own. null means no expiration.
defaultSlidingTtlbooleanfalseWhether the default TTL mode is sliding (resets on access) or fixed (from creation).
sweepIntervalMsnumber | nullnullInterval in milliseconds for proactive background sweep of expired entries. null disables active sweep.
now() => numberDate.nowCustom time source. Useful for deterministic testing.
persistencePersistenceAdapterundefinedOptional adapter for saving and loading scratchpad state.

Returns:Scratchpad


fromSnapshot(snapshot, options?)

Creates a new Scratchpad pre-populated from a previously captured snapshot.

import{fromSnapshot}from'agent-scratchpad';constpad=fromSnapshot(snap,{defaultTtl: 30_000});

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot object previously obtained from pad.snapshot() or pad.serialize().
  • options (ScratchpadOptions, optional) -- Configuration options passed to the underlying createScratchpad() call.

Returns:Scratchpad


Scratchpad Methods

set<T>(key, value, options?)

Stores a value under the given key. If the key already exists, updates the value and updatedAt timestamp while preserving createdAt.

pad.set('result',{score: 0.95});pad.set('cache','value',{ttl: 5_000,tags: ['temporary']});pad.set('session',token,{ttl: 30_000,slidingTtl: true});

Parameters:

  • key (string) -- The entry key.
  • value (T) -- The value to store.
  • options (EntryOptions, optional) -- Per-entry configuration.
OptionTypeDefaultDescription
ttlnumber | nullInherits defaultTtlTTL in milliseconds. null disables expiration for this entry.
slidingTtlbooleanInherits defaultSlidingTtlWhether TTL resets on each get() access.
tagsstring[][]String labels for categorizing the entry.

Returns:void

get<T>(key)

Retrieves the value for a key. If the entry has expired, it is removed, an expire event fires, and undefined is returned. On a successful read, accessedAt is updated (which resets the sliding TTL window if applicable).

constuser=pad.get<{id: number;name: string}>('user');

Parameters:

  • key (string) -- The entry key.

Returns:T | undefined

has(key)

Checks whether a key exists and is not expired. Expired entries are removed and trigger an expire event.

if(pad.has('apiResponse')){// entry is live}

Parameters:

  • key (string) -- The entry key.

Returns:boolean

delete(key)

Removes an entry by key. Fires a delete event if the entry existed.

constremoved=pad.delete('staleData');// true if it existed

Parameters:

  • key (string) -- The entry key.

Returns:boolean -- true if the entry existed and was removed, false otherwise.

clear()

Removes all entries from the scratchpad. Fires a clear event with the count of removed entries.

pad.clear();

Returns:void

keys()

Returns an array of all non-expired keys. Expired entries encountered during iteration are excluded.

constallKeys=pad.keys();// ['step', 'user', 'result']

Returns:string[]

entries()

Returns an array of [key, ScratchpadEntry] tuples for all non-expired entries.

for(const[key,entry]ofpad.entries()){console.log(key,entry.value,entry.tags);}

Returns:[string, ScratchpadEntry][]

findByTag(tag)

Returns all non-expired entries whose tags array includes the given tag (exact match).

pad.set('london','UK capital',{tags: ['geo','important']});pad.set('paris','France capital',{tags: ['geo']});constgeoEntries=pad.findByTag('geo');// both entries

Parameters:

  • tag (string) -- The tag to search for.

Returns:ScratchpadEntry[]

namespace(name)

Returns a scoped view of the scratchpad where all operations are prefixed with name:. Namespaces share the underlying storage with the parent -- they are views, not copies. Namespaces can be nested.

constmemory=pad.namespace('memory');memory.set('fact','The sky is blue');memory.get('fact');// 'The sky is blue'pad.get('memory:fact');// 'The sky is blue'// Nested namespaces compose prefixesconstdeep=pad.namespace('a').namespace('b');deep.set('key','val');pad.get('a:b:key');// 'val'// Namespace-scoped operationsmemory.keys();// ['fact'] (prefix stripped)memory.clear();// removes only memory:* entries

Parameters:

  • name (string) -- The namespace prefix.

Returns:Scratchpad -- A namespace-scoped scratchpad instance with the same full API.

snapshot()

Captures the full scratchpad state at the current point in time. The returned snapshot is a plain object suitable for serialization.

constsnap=pad.snapshot();// { entries: { ... }, timestamp: 1710000000000, version: 1 }

Returns:ScratchpadSnapshot

restore(snapshot)

Replaces the entire scratchpad state with the contents of a snapshot. Clears all existing entries before restoring. Throws ScratchpadVersionError if the snapshot version is not supported.

pad.restore(snap);

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot previously obtained from snapshot() or serialize().

Returns:void

Throws:ScratchpadVersionError if snapshot.version is not 1.

serialize()

Alias for snapshot(). Returns the same ScratchpadSnapshot structure.

constdata=pad.serialize();

Returns:ScratchpadSnapshot

toContext(options?)

Renders scratchpad contents as a formatted string suitable for injection into an LLM prompt. Supports filtering by tags or namespace, multiple output formats, token budget limits, and custom headers.

pad.set('name','Alice');pad.set('role','admin');pad.toContext();// 'name: Alice\nrole: admin'pad.toContext({format: 'markdown'});// '## name\nAlice\n\n## role\nadmin'pad.toContext({format: 'json'});// '{"name":"Alice","role":"admin"}'pad.toContext({format: 'xml'});// '<entry key="name">Alice</entry>\n<entry key="role">admin</entry>'

Parameters:

OptionTypeDefaultDescription
format'kv' | 'markdown' | 'xml' | 'json''kv'Output format.
filterTagsstring[]undefinedOnly include entries that have at least one of the specified tags.
filterNamespacestringundefinedOnly include entries whose key starts with the given namespace prefix.
maxTokensnumberundefinedTruncate output to fit within this token budget.
tokenCounter(text: string) => numbertext.lengthFunction to count tokens. Used with maxTokens.
includeMetadatabooleanundefinedReserved for future use.
headerstringundefinedText prepended to the output before the formatted entries.

Returns:string

stats()

Returns aggregate statistics about the scratchpad's current state.

constst=pad.stats();// {// size: 3, // live (non-expired) entry count// rawSize: 4, // total entries including expired-not-yet-swept// namespaceCount: 2,// namespaces: ['ctx', 'mem'],// entriesWithTtl: 1,// tagCounts: { geo: 2, important: 1 },// oldestEntryAt: 1710000000000,// newestEntryAt: 1710000001000,// }

Returns:ScratchpadStats

FieldTypeDescription
sizenumberCount of non-expired entries.
rawSizenumberTotal entries in the store, including expired entries not yet swept.
namespaceCountnumberNumber of distinct namespace prefixes.
namespacesstring[]List of distinct namespace prefixes.
entriesWithTtlnumberCount of entries that have a TTL set.
tagCountsRecord<string, number>Count of entries per tag.
oldestEntryAtnumber | nullcreatedAt of the oldest live entry, or null if empty.
newestEntryAtnumber | nullcreatedAt of the newest live entry, or null if empty.

on(event, handler)

Registers an event handler. Returns an unsubscribe function.

constunsub=pad.on('set',({ key, entry, isUpdate })=>{console.log(isUpdate ? 'updated' : 'created',key);});pad.on('delete',({ key, entry })=>{console.log('deleted',key);});pad.on('expire',({ key, entry })=>{console.log('expired',key);});pad.on('clear',({ count })=>{console.log('cleared',count,'entries');});// Stop listeningunsub();

Parameters:

  • event (ScratchpadEventName) -- One of 'set', 'delete', 'expire', 'clear'.
  • handler (ScratchpadEventHandler<K>) -- Callback receiving the event payload.

Event Payloads:

EventPayload
set{ key: string; entry: ScratchpadEntry; isUpdate: boolean }
delete{ key: string; entry: ScratchpadEntry }
expire{ key: string; entry: ScratchpadEntry }
clear{ count: number }

Returns:() => void -- Call to unsubscribe.

save()

Persists the current scratchpad state using the configured PersistenceAdapter. No-op if no adapter was provided.

awaitpad.save();

Returns:Promise<void>

load()

Loads scratchpad state from the configured PersistenceAdapter and restores it. No-op if no adapter was provided or the adapter returns null.

awaitpad.load();

Returns:Promise<void>

destroy()

Cleans up resources. Stops the background sweep timer if one is running.

awaitpad.destroy();

Returns:Promise<void>


TTL Utility Functions

isExpired(entry, now)

Determines whether a scratchpad entry has expired based on its TTL configuration.

import{isExpired}from'agent-scratchpad';constexpired=isExpired(entry,Date.now());

Parameters:

  • entry (ScratchpadEntry) -- The entry to check.
  • now (number) -- Current timestamp in milliseconds.

Returns:boolean -- true if the entry's TTL has elapsed.

Logic:

  • Returns false if entry.ttl is null.
  • For fixed TTL (slidingTtl: false): expired when now >= entry.createdAt + entry.ttl.
  • For sliding TTL (slidingTtl: true): expired when now >= entry.accessedAt + entry.ttl.

expiresAt(entry)

Calculates the absolute expiration timestamp for an entry.

import{expiresAt}from'agent-scratchpad';constexpiry=expiresAt(entry);// number | null

Parameters:

  • entry (ScratchpadEntry) -- The entry to inspect.

Returns:number | null -- The Unix timestamp (ms) when the entry expires, or null if it has no TTL.


Types

ScratchpadEntry<T>

interfaceScratchpadEntry<T=unknown>{key: string;value: T;createdAt: number;// Unix ms when first createdupdatedAt: number;// Unix ms when value last updatedaccessedAt: number;// Unix ms when last read via get()ttl: number|null;// TTL in ms, null = no expirationslidingTtl: boolean;// true = TTL resets on accesstags: string[];// string labels for categorization}

EntryOptions

interfaceEntryOptions{ttl?: number|null;slidingTtl?: boolean;tags?: string[];}

ScratchpadOptions

interfaceScratchpadOptions{defaultTtl?: number|null;defaultSlidingTtl?: boolean;sweepIntervalMs?: number|null;now?: ()=>number;persistence?: PersistenceAdapter;}

ScratchpadSnapshot

interfaceScratchpadSnapshot{entries: Record<string,ScratchpadEntry>;timestamp: number;version: 1;}

ScratchpadStats

interfaceScratchpadStats{size: number;rawSize: number;namespaceCount: number;namespaces: string[];entriesWithTtl: number;tagCounts: Record<string,number>;oldestEntryAt: number|null;newestEntryAt: number|null;}

ToContextOptions

interfaceToContextOptions{format?: 'markdown'|'xml'|'json'|'kv';filterTags?: string[];filterNamespace?: string;maxTokens?: number;tokenCounter?: (text: string)=>number;includeMetadata?: boolean;header?: string;}

PersistenceAdapter

interfacePersistenceAdapter{load(): Promise<ScratchpadSnapshot|null>;save(snap: ScratchpadSnapshot): Promise<void>;}

ScratchpadEvents

interfaceScratchpadEvents{set: {key: string;entry: ScratchpadEntry;isUpdate: boolean};delete: {key: string;entry: ScratchpadEntry};expire: {key: string;entry: ScratchpadEntry};clear: {count: number};}

ScratchpadEventName

typeScratchpadEventName='set'|'delete'|'expire'|'clear';

ScratchpadEventHandler<K>

typeScratchpadEventHandler<KextendsScratchpadEventName>=(data: ScratchpadEvents[K])=>void;

Error Handling

agent-scratchpad exports three error classes, all extending a common base.

ScratchpadError

Base class for all scratchpad errors. Extends Error with a code property.

import{ScratchpadError}from'agent-scratchpad';try{pad.restore(badSnapshot);}catch(err){if(errinstanceofScratchpadError){console.error(err.code);// e.g. 'SCRATCHPAD_VERSION_ERROR'console.error(err.message);// human-readable description}}
PropertyTypeDescription
codestringMachine-readable error code.
messagestringHuman-readable error description.
namestringAlways 'ScratchpadError'.

ScratchpadConfigError

Thrown when invalid configuration is provided to createScratchpad().

  • Code:SCRATCHPAD_CONFIG_ERROR

ScratchpadVersionError

Thrown when restore() encounters a snapshot with an unsupported version number.

  • Code:SCRATCHPAD_VERSION_ERROR
  • Additional property:version (number) -- The unsupported version that was encountered.
import{ScratchpadVersionError}from'agent-scratchpad';try{pad.restore(snap);}catch(err){if(errinstanceofScratchpadVersionError){console.error(`Unsupported version: ${err.version}`);}}

Advanced Usage

Agent Working Memory in a ReAct Loop

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 300_000});// 5-minute default// Step 1: Store tool outputpad.set('search:result',apiResponse,{tags: ['tool-result','search']});// Step 2: Extract and store entitiespad.set('entities:user',{name: 'Alice',id: 42},{tags: ['entity']});pad.set('entities:order',{orderId: '#12345'},{tags: ['entity']});// Step 3: Inject scratchpad into promptconstagentContext=pad.toContext({format: 'markdown',header: '## Agent Working Memory',filterTags: ['entity'],});// Produces:// ## Agent Working Memory// ## entities:user// [object Object]// ...

Namespace Isolation for Multi-Agent Systems

constpad=createScratchpad();constagent1=pad.namespace('agent1');constagent2=pad.namespace('agent2');agent1.set('plan','Research the topic');agent2.set('plan','Draft the response');// Each agent sees only its own entriesagent1.keys();// ['plan']agent2.keys();// ['plan']// Parent sees all entries with prefixed keyspad.keys();// ['agent1:plan', 'agent2:plan']// Clear one agent without affecting the otheragent1.clear();agent2.keys();// ['plan'] -- unaffected

Sliding TTL for Session-Like Data

constpad=createScratchpad();// Session token stays alive as long as the agent keeps accessing itpad.set('session',{token: 'abc123'},{ttl: 30_000,slidingTtl: true});// Each access resets the 30-second expiration windowpad.get('session');// resets timerpad.get('session');// resets timer again// If 30 seconds pass without access, the entry expires

Background Sweep with Event Logging

constpad=createScratchpad({sweepIntervalMs: 10_000});pad.on('expire',({ key, entry })=>{console.log(`Expired: ${key} (created ${newDate(entry.createdAt).toISOString()})`);});pad.set('temp','data',{ttl: 15_000});// The sweep timer runs every 10 seconds and removes expired entries.// The expire event fires for each entry removed by the sweep.// Stop the sweep timer when doneawaitpad.destroy();

Persistence with a File-Based Adapter

import{createScratchpad,PersistenceAdapter,ScratchpadSnapshot}from'agent-scratchpad';importfsfrom'fs/promises';constfileAdapter: PersistenceAdapter={asyncload(){try{constdata=awaitfs.readFile('scratchpad.json','utf8');returnJSON.parse(data)asScratchpadSnapshot;}catch{returnnull;}},asyncsave(snap){awaitfs.writeFile('scratchpad.json',JSON.stringify(snap,null,2));},};constpad=createScratchpad({persistence: fileAdapter});// Restore previous state on startupawaitpad.load();// Work with the scratchpadpad.set('progress','step-3');// Persist state before shutdownawaitpad.save();

Snapshot-Based Backtracking

constpad=createScratchpad();pad.set('approach','strategy-A');pad.set('findings',['result-1']);// Save state before trying something riskyconstcheckpoint=pad.snapshot();pad.set('approach','strategy-B');pad.set('findings',['result-1','result-2-failed']);// Strategy B failed -- roll backpad.restore(checkpoint);pad.get('approach');// 'strategy-A'

Token-Budget-Aware Context Rendering

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();pad.set('summary','A long summary of findings...');pad.set('details','Extensive details that may not fit...');// Use a custom token counter (e.g., tiktoken)constcontext=pad.toContext({format: 'kv',maxTokens: 500,tokenCounter: (text)=>Math.ceil(text.length/4),// rough estimateheader: '## Working Memory',});

Deterministic Testing with Custom Time

import{createScratchpad}from'agent-scratchpad';letnow=0;constpad=createScratchpad({now: ()=>now});pad.set('key','value',{ttl: 100});now=50;pad.get('key');// 'value' -- still alivenow=100;pad.get('key');// undefined -- expired exactly at 100ms

TypeScript

agent-scratchpad is written in TypeScript and ships with full type declarations. All exported functions, interfaces, and types are available for import:

import{createScratchpad,fromSnapshot,toContext,isExpired,expiresAt,ScratchpadError,ScratchpadConfigError,ScratchpadVersionError,}from'agent-scratchpad';importtype{Scratchpad,ScratchpadEntry,ScratchpadOptions,ScratchpadSnapshot,ScratchpadStats,ToContextOptions,EntryOptions,ScratchpadEventName,ScratchpadEventHandler,ScratchpadEvents,PersistenceAdapter,}from'agent-scratchpad';

Generic type parameters on get<T>() and set<T>() provide type-safe value access without casts:

interfaceUser{id: number;name: string;}pad.set<User>('user',{id: 1,name: 'Alice'});constuser=pad.get<User>('user');// user is User | undefined

License

MIT

About

Lightweight key-value scratchpad for agent reasoning

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SiluPanda/agent-scratchpad: Lightweight key-value scratchpad for agent reasoning · GitHub
Skip to content

Repository files navigation

agent-scratchpad

Lightweight key-value scratchpad for AI agent working memory.

npm versionnpm downloadslicensenodeTypeScript

agent-scratchpad is a zero-dependency, in-process key-value store purpose-built for AI agent reasoning loops. Agents executing multi-step workflows (ReAct, Plan-and-Execute, Chain-of-Thought with tool use) need a place to write down intermediate state between steps -- tool outputs, extracted entities, partial computations, decision rationale, and task decomposition state. This package provides that working memory with typed entries, automatic TTL-based expiration, hierarchical namespaces, tag-based querying, point-in-time snapshots, event-driven change observation, pluggable persistence, and a toContext() method that renders scratchpad contents directly into LLM prompts. It works with any agent framework or custom agent loop.

Installation

npm install agent-scratchpad

Quick Start

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();// Store intermediate resultspad.set('step','analyze');pad.set('user',{id: 42,name: 'Alice'});console.log(pad.get('step'));// 'analyze'console.log(pad.has('user'));// trueconsole.log(pad.keys());// ['step', 'user']// Render contents for an LLM promptconstcontext=pad.toContext({format: 'markdown'});

Features

  • Zero runtime dependencies -- all logic uses built-in JavaScript APIs
  • TypeScript-first -- full generic type safety on get<T>() and set<T>()
  • TTL expiration -- fixed or sliding time-to-live with lazy and active sweep modes
  • Hierarchical namespaces -- scope entries per agent, task, or step with pad.namespace('name')
  • Tag-based querying -- label entries and retrieve them with findByTag()
  • Snapshots -- capture and restore full scratchpad state for backtracking and debugging
  • Context rendering -- format entries as Markdown, XML, JSON, or key-value pairs for LLM prompts
  • Event system -- observe set, delete, expire, and clear events
  • Pluggable persistence -- save and load scratchpad state via a simple adapter interface
  • Framework-agnostic -- works with LangChain, Vercel AI SDK, AutoGen, CrewAI, or any custom loop

API Reference

createScratchpad(options?)

Creates a new Scratchpad instance.

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 60_000,defaultSlidingTtl: false,sweepIntervalMs: 10_000,now: ()=>Date.now(),persistence: adapter,});

Parameters:

OptionTypeDefaultDescription
defaultTtlnumber | nullnullDefault TTL in milliseconds applied to entries that do not specify their own. null means no expiration.
defaultSlidingTtlbooleanfalseWhether the default TTL mode is sliding (resets on access) or fixed (from creation).
sweepIntervalMsnumber | nullnullInterval in milliseconds for proactive background sweep of expired entries. null disables active sweep.
now() => numberDate.nowCustom time source. Useful for deterministic testing.
persistencePersistenceAdapterundefinedOptional adapter for saving and loading scratchpad state.

Returns:Scratchpad


fromSnapshot(snapshot, options?)

Creates a new Scratchpad pre-populated from a previously captured snapshot.

import{fromSnapshot}from'agent-scratchpad';constpad=fromSnapshot(snap,{defaultTtl: 30_000});

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot object previously obtained from pad.snapshot() or pad.serialize().
  • options (ScratchpadOptions, optional) -- Configuration options passed to the underlying createScratchpad() call.

Returns:Scratchpad


Scratchpad Methods

set<T>(key, value, options?)

Stores a value under the given key. If the key already exists, updates the value and updatedAt timestamp while preserving createdAt.

pad.set('result',{score: 0.95});pad.set('cache','value',{ttl: 5_000,tags: ['temporary']});pad.set('session',token,{ttl: 30_000,slidingTtl: true});

Parameters:

  • key (string) -- The entry key.
  • value (T) -- The value to store.
  • options (EntryOptions, optional) -- Per-entry configuration.
OptionTypeDefaultDescription
ttlnumber | nullInherits defaultTtlTTL in milliseconds. null disables expiration for this entry.
slidingTtlbooleanInherits defaultSlidingTtlWhether TTL resets on each get() access.
tagsstring[][]String labels for categorizing the entry.

Returns:void

get<T>(key)

Retrieves the value for a key. If the entry has expired, it is removed, an expire event fires, and undefined is returned. On a successful read, accessedAt is updated (which resets the sliding TTL window if applicable).

constuser=pad.get<{id: number;name: string}>('user');

Parameters:

  • key (string) -- The entry key.

Returns:T | undefined

has(key)

Checks whether a key exists and is not expired. Expired entries are removed and trigger an expire event.

if(pad.has('apiResponse')){// entry is live}

Parameters:

  • key (string) -- The entry key.

Returns:boolean

delete(key)

Removes an entry by key. Fires a delete event if the entry existed.

constremoved=pad.delete('staleData');// true if it existed

Parameters:

  • key (string) -- The entry key.

Returns:boolean -- true if the entry existed and was removed, false otherwise.

clear()

Removes all entries from the scratchpad. Fires a clear event with the count of removed entries.

pad.clear();

Returns:void

keys()

Returns an array of all non-expired keys. Expired entries encountered during iteration are excluded.

constallKeys=pad.keys();// ['step', 'user', 'result']

Returns:string[]

entries()

Returns an array of [key, ScratchpadEntry] tuples for all non-expired entries.

for(const[key,entry]ofpad.entries()){console.log(key,entry.value,entry.tags);}

Returns:[string, ScratchpadEntry][]

findByTag(tag)

Returns all non-expired entries whose tags array includes the given tag (exact match).

pad.set('london','UK capital',{tags: ['geo','important']});pad.set('paris','France capital',{tags: ['geo']});constgeoEntries=pad.findByTag('geo');// both entries

Parameters:

  • tag (string) -- The tag to search for.

Returns:ScratchpadEntry[]

namespace(name)

Returns a scoped view of the scratchpad where all operations are prefixed with name:. Namespaces share the underlying storage with the parent -- they are views, not copies. Namespaces can be nested.

constmemory=pad.namespace('memory');memory.set('fact','The sky is blue');memory.get('fact');// 'The sky is blue'pad.get('memory:fact');// 'The sky is blue'// Nested namespaces compose prefixesconstdeep=pad.namespace('a').namespace('b');deep.set('key','val');pad.get('a:b:key');// 'val'// Namespace-scoped operationsmemory.keys();// ['fact'] (prefix stripped)memory.clear();// removes only memory:* entries

Parameters:

  • name (string) -- The namespace prefix.

Returns:Scratchpad -- A namespace-scoped scratchpad instance with the same full API.

snapshot()

Captures the full scratchpad state at the current point in time. The returned snapshot is a plain object suitable for serialization.

constsnap=pad.snapshot();// { entries: { ... }, timestamp: 1710000000000, version: 1 }

Returns:ScratchpadSnapshot

restore(snapshot)

Replaces the entire scratchpad state with the contents of a snapshot. Clears all existing entries before restoring. Throws ScratchpadVersionError if the snapshot version is not supported.

pad.restore(snap);

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot previously obtained from snapshot() or serialize().

Returns:void

Throws:ScratchpadVersionError if snapshot.version is not 1.

serialize()

Alias for snapshot(). Returns the same ScratchpadSnapshot structure.

constdata=pad.serialize();

Returns:ScratchpadSnapshot

toContext(options?)

Renders scratchpad contents as a formatted string suitable for injection into an LLM prompt. Supports filtering by tags or namespace, multiple output formats, token budget limits, and custom headers.

pad.set('name','Alice');pad.set('role','admin');pad.toContext();// 'name: Alice\nrole: admin'pad.toContext({format: 'markdown'});// '## name\nAlice\n\n## role\nadmin'pad.toContext({format: 'json'});// '{"name":"Alice","role":"admin"}'pad.toContext({format: 'xml'});// '<entry key="name">Alice</entry>\n<entry key="role">admin</entry>'

Parameters:

OptionTypeDefaultDescription
format'kv' | 'markdown' | 'xml' | 'json''kv'Output format.
filterTagsstring[]undefinedOnly include entries that have at least one of the specified tags.
filterNamespacestringundefinedOnly include entries whose key starts with the given namespace prefix.
maxTokensnumberundefinedTruncate output to fit within this token budget.
tokenCounter(text: string) => numbertext.lengthFunction to count tokens. Used with maxTokens.
includeMetadatabooleanundefinedReserved for future use.
headerstringundefinedText prepended to the output before the formatted entries.

Returns:string

stats()

Returns aggregate statistics about the scratchpad's current state.

constst=pad.stats();// {// size: 3, // live (non-expired) entry count// rawSize: 4, // total entries including expired-not-yet-swept// namespaceCount: 2,// namespaces: ['ctx', 'mem'],// entriesWithTtl: 1,// tagCounts: { geo: 2, important: 1 },// oldestEntryAt: 1710000000000,// newestEntryAt: 1710000001000,// }

Returns:ScratchpadStats

FieldTypeDescription
sizenumberCount of non-expired entries.
rawSizenumberTotal entries in the store, including expired entries not yet swept.
namespaceCountnumberNumber of distinct namespace prefixes.
namespacesstring[]List of distinct namespace prefixes.
entriesWithTtlnumberCount of entries that have a TTL set.
tagCountsRecord<string, number>Count of entries per tag.
oldestEntryAtnumber | nullcreatedAt of the oldest live entry, or null if empty.
newestEntryAtnumber | nullcreatedAt of the newest live entry, or null if empty.

on(event, handler)

Registers an event handler. Returns an unsubscribe function.

constunsub=pad.on('set',({ key, entry, isUpdate })=>{console.log(isUpdate ? 'updated' : 'created',key);});pad.on('delete',({ key, entry })=>{console.log('deleted',key);});pad.on('expire',({ key, entry })=>{console.log('expired',key);});pad.on('clear',({ count })=>{console.log('cleared',count,'entries');});// Stop listeningunsub();

Parameters:

  • event (ScratchpadEventName) -- One of 'set', 'delete', 'expire', 'clear'.
  • handler (ScratchpadEventHandler<K>) -- Callback receiving the event payload.

Event Payloads:

EventPayload
set{ key: string; entry: ScratchpadEntry; isUpdate: boolean }
delete{ key: string; entry: ScratchpadEntry }
expire{ key: string; entry: ScratchpadEntry }
clear{ count: number }

Returns:() => void -- Call to unsubscribe.

save()

Persists the current scratchpad state using the configured PersistenceAdapter. No-op if no adapter was provided.

awaitpad.save();

Returns:Promise<void>

load()

Loads scratchpad state from the configured PersistenceAdapter and restores it. No-op if no adapter was provided or the adapter returns null.

awaitpad.load();

Returns:Promise<void>

destroy()

Cleans up resources. Stops the background sweep timer if one is running.

awaitpad.destroy();

Returns:Promise<void>


TTL Utility Functions

isExpired(entry, now)

Determines whether a scratchpad entry has expired based on its TTL configuration.

import{isExpired}from'agent-scratchpad';constexpired=isExpired(entry,Date.now());

Parameters:

  • entry (ScratchpadEntry) -- The entry to check.
  • now (number) -- Current timestamp in milliseconds.

Returns:boolean -- true if the entry's TTL has elapsed.

Logic:

  • Returns false if entry.ttl is null.
  • For fixed TTL (slidingTtl: false): expired when now >= entry.createdAt + entry.ttl.
  • For sliding TTL (slidingTtl: true): expired when now >= entry.accessedAt + entry.ttl.

expiresAt(entry)

Calculates the absolute expiration timestamp for an entry.

import{expiresAt}from'agent-scratchpad';constexpiry=expiresAt(entry);// number | null

Parameters:

  • entry (ScratchpadEntry) -- The entry to inspect.

Returns:number | null -- The Unix timestamp (ms) when the entry expires, or null if it has no TTL.


Types

ScratchpadEntry<T>

interfaceScratchpadEntry<T=unknown>{key: string;value: T;createdAt: number;// Unix ms when first createdupdatedAt: number;// Unix ms when value last updatedaccessedAt: number;// Unix ms when last read via get()ttl: number|null;// TTL in ms, null = no expirationslidingTtl: boolean;// true = TTL resets on accesstags: string[];// string labels for categorization}

EntryOptions

interfaceEntryOptions{ttl?: number|null;slidingTtl?: boolean;tags?: string[];}

ScratchpadOptions

interfaceScratchpadOptions{defaultTtl?: number|null;defaultSlidingTtl?: boolean;sweepIntervalMs?: number|null;now?: ()=>number;persistence?: PersistenceAdapter;}

ScratchpadSnapshot

interfaceScratchpadSnapshot{entries: Record<string,ScratchpadEntry>;timestamp: number;version: 1;}

ScratchpadStats

interfaceScratchpadStats{size: number;rawSize: number;namespaceCount: number;namespaces: string[];entriesWithTtl: number;tagCounts: Record<string,number>;oldestEntryAt: number|null;newestEntryAt: number|null;}

ToContextOptions

interfaceToContextOptions{format?: 'markdown'|'xml'|'json'|'kv';filterTags?: string[];filterNamespace?: string;maxTokens?: number;tokenCounter?: (text: string)=>number;includeMetadata?: boolean;header?: string;}

PersistenceAdapter

interfacePersistenceAdapter{load(): Promise<ScratchpadSnapshot|null>;save(snap: ScratchpadSnapshot): Promise<void>;}

ScratchpadEvents

interfaceScratchpadEvents{set: {key: string;entry: ScratchpadEntry;isUpdate: boolean};delete: {key: string;entry: ScratchpadEntry};expire: {key: string;entry: ScratchpadEntry};clear: {count: number};}

ScratchpadEventName

typeScratchpadEventName='set'|'delete'|'expire'|'clear';

ScratchpadEventHandler<K>

typeScratchpadEventHandler<KextendsScratchpadEventName>=(data: ScratchpadEvents[K])=>void;

Error Handling

agent-scratchpad exports three error classes, all extending a common base.

ScratchpadError

Base class for all scratchpad errors. Extends Error with a code property.

import{ScratchpadError}from'agent-scratchpad';try{pad.restore(badSnapshot);}catch(err){if(errinstanceofScratchpadError){console.error(err.code);// e.g. 'SCRATCHPAD_VERSION_ERROR'console.error(err.message);// human-readable description}}
PropertyTypeDescription
codestringMachine-readable error code.
messagestringHuman-readable error description.
namestringAlways 'ScratchpadError'.

ScratchpadConfigError

Thrown when invalid configuration is provided to createScratchpad().

  • Code:SCRATCHPAD_CONFIG_ERROR

ScratchpadVersionError

Thrown when restore() encounters a snapshot with an unsupported version number.

  • Code:SCRATCHPAD_VERSION_ERROR
  • Additional property:version (number) -- The unsupported version that was encountered.
import{ScratchpadVersionError}from'agent-scratchpad';try{pad.restore(snap);}catch(err){if(errinstanceofScratchpadVersionError){console.error(`Unsupported version: ${err.version}`);}}

Advanced Usage

Agent Working Memory in a ReAct Loop

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 300_000});// 5-minute default// Step 1: Store tool outputpad.set('search:result',apiResponse,{tags: ['tool-result','search']});// Step 2: Extract and store entitiespad.set('entities:user',{name: 'Alice',id: 42},{tags: ['entity']});pad.set('entities:order',{orderId: '#12345'},{tags: ['entity']});// Step 3: Inject scratchpad into promptconstagentContext=pad.toContext({format: 'markdown',header: '## Agent Working Memory',filterTags: ['entity'],});// Produces:// ## Agent Working Memory// ## entities:user// [object Object]// ...

Namespace Isolation for Multi-Agent Systems

constpad=createScratchpad();constagent1=pad.namespace('agent1');constagent2=pad.namespace('agent2');agent1.set('plan','Research the topic');agent2.set('plan','Draft the response');// Each agent sees only its own entriesagent1.keys();// ['plan']agent2.keys();// ['plan']// Parent sees all entries with prefixed keyspad.keys();// ['agent1:plan', 'agent2:plan']// Clear one agent without affecting the otheragent1.clear();agent2.keys();// ['plan'] -- unaffected

Sliding TTL for Session-Like Data

constpad=createScratchpad();// Session token stays alive as long as the agent keeps accessing itpad.set('session',{token: 'abc123'},{ttl: 30_000,slidingTtl: true});// Each access resets the 30-second expiration windowpad.get('session');// resets timerpad.get('session');// resets timer again// If 30 seconds pass without access, the entry expires

Background Sweep with Event Logging

constpad=createScratchpad({sweepIntervalMs: 10_000});pad.on('expire',({ key, entry })=>{console.log(`Expired: ${key} (created ${newDate(entry.createdAt).toISOString()})`);});pad.set('temp','data',{ttl: 15_000});// The sweep timer runs every 10 seconds and removes expired entries.// The expire event fires for each entry removed by the sweep.// Stop the sweep timer when doneawaitpad.destroy();

Persistence with a File-Based Adapter

import{createScratchpad,PersistenceAdapter,ScratchpadSnapshot}from'agent-scratchpad';importfsfrom'fs/promises';constfileAdapter: PersistenceAdapter={asyncload(){try{constdata=awaitfs.readFile('scratchpad.json','utf8');returnJSON.parse(data)asScratchpadSnapshot;}catch{returnnull;}},asyncsave(snap){awaitfs.writeFile('scratchpad.json',JSON.stringify(snap,null,2));},};constpad=createScratchpad({persistence: fileAdapter});// Restore previous state on startupawaitpad.load();// Work with the scratchpadpad.set('progress','step-3');// Persist state before shutdownawaitpad.save();

Snapshot-Based Backtracking

constpad=createScratchpad();pad.set('approach','strategy-A');pad.set('findings',['result-1']);// Save state before trying something riskyconstcheckpoint=pad.snapshot();pad.set('approach','strategy-B');pad.set('findings',['result-1','result-2-failed']);// Strategy B failed -- roll backpad.restore(checkpoint);pad.get('approach');// 'strategy-A'

Token-Budget-Aware Context Rendering

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();pad.set('summary','A long summary of findings...');pad.set('details','Extensive details that may not fit...');// Use a custom token counter (e.g., tiktoken)constcontext=pad.toContext({format: 'kv',maxTokens: 500,tokenCounter: (text)=>Math.ceil(text.length/4),// rough estimateheader: '## Working Memory',});

Deterministic Testing with Custom Time

import{createScratchpad}from'agent-scratchpad';letnow=0;constpad=createScratchpad({now: ()=>now});pad.set('key','value',{ttl: 100});now=50;pad.get('key');// 'value' -- still alivenow=100;pad.get('key');// undefined -- expired exactly at 100ms

TypeScript

agent-scratchpad is written in TypeScript and ships with full type declarations. All exported functions, interfaces, and types are available for import:

import{createScratchpad,fromSnapshot,toContext,isExpired,expiresAt,ScratchpadError,ScratchpadConfigError,ScratchpadVersionError,}from'agent-scratchpad';importtype{Scratchpad,ScratchpadEntry,ScratchpadOptions,ScratchpadSnapshot,ScratchpadStats,ToContextOptions,EntryOptions,ScratchpadEventName,ScratchpadEventHandler,ScratchpadEvents,PersistenceAdapter,}from'agent-scratchpad';

Generic type parameters on get<T>() and set<T>() provide type-safe value access without casts:

interfaceUser{id: number;name: string;}pad.set<User>('user',{id: 1,name: 'Alice'});constuser=pad.get<User>('user');// user is User | undefined

License

MIT

About

Lightweight key-value scratchpad for agent reasoning

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - SiluPanda/agent-scratchpad: Lightweight key-value scratchpad for agent reasoning · GitHub
Skip to content

Repository files navigation

agent-scratchpad

Lightweight key-value scratchpad for AI agent working memory.

npm versionnpm downloadslicensenodeTypeScript

agent-scratchpad is a zero-dependency, in-process key-value store purpose-built for AI agent reasoning loops. Agents executing multi-step workflows (ReAct, Plan-and-Execute, Chain-of-Thought with tool use) need a place to write down intermediate state between steps -- tool outputs, extracted entities, partial computations, decision rationale, and task decomposition state. This package provides that working memory with typed entries, automatic TTL-based expiration, hierarchical namespaces, tag-based querying, point-in-time snapshots, event-driven change observation, pluggable persistence, and a toContext() method that renders scratchpad contents directly into LLM prompts. It works with any agent framework or custom agent loop.

Installation

npm install agent-scratchpad

Quick Start

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();// Store intermediate resultspad.set('step','analyze');pad.set('user',{id: 42,name: 'Alice'});console.log(pad.get('step'));// 'analyze'console.log(pad.has('user'));// trueconsole.log(pad.keys());// ['step', 'user']// Render contents for an LLM promptconstcontext=pad.toContext({format: 'markdown'});

Features

  • Zero runtime dependencies -- all logic uses built-in JavaScript APIs
  • TypeScript-first -- full generic type safety on get<T>() and set<T>()
  • TTL expiration -- fixed or sliding time-to-live with lazy and active sweep modes
  • Hierarchical namespaces -- scope entries per agent, task, or step with pad.namespace('name')
  • Tag-based querying -- label entries and retrieve them with findByTag()
  • Snapshots -- capture and restore full scratchpad state for backtracking and debugging
  • Context rendering -- format entries as Markdown, XML, JSON, or key-value pairs for LLM prompts
  • Event system -- observe set, delete, expire, and clear events
  • Pluggable persistence -- save and load scratchpad state via a simple adapter interface
  • Framework-agnostic -- works with LangChain, Vercel AI SDK, AutoGen, CrewAI, or any custom loop

API Reference

createScratchpad(options?)

Creates a new Scratchpad instance.

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 60_000,defaultSlidingTtl: false,sweepIntervalMs: 10_000,now: ()=>Date.now(),persistence: adapter,});

Parameters:

OptionTypeDefaultDescription
defaultTtlnumber | nullnullDefault TTL in milliseconds applied to entries that do not specify their own. null means no expiration.
defaultSlidingTtlbooleanfalseWhether the default TTL mode is sliding (resets on access) or fixed (from creation).
sweepIntervalMsnumber | nullnullInterval in milliseconds for proactive background sweep of expired entries. null disables active sweep.
now() => numberDate.nowCustom time source. Useful for deterministic testing.
persistencePersistenceAdapterundefinedOptional adapter for saving and loading scratchpad state.

Returns:Scratchpad


fromSnapshot(snapshot, options?)

Creates a new Scratchpad pre-populated from a previously captured snapshot.

import{fromSnapshot}from'agent-scratchpad';constpad=fromSnapshot(snap,{defaultTtl: 30_000});

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot object previously obtained from pad.snapshot() or pad.serialize().
  • options (ScratchpadOptions, optional) -- Configuration options passed to the underlying createScratchpad() call.

Returns:Scratchpad


Scratchpad Methods

set<T>(key, value, options?)

Stores a value under the given key. If the key already exists, updates the value and updatedAt timestamp while preserving createdAt.

pad.set('result',{score: 0.95});pad.set('cache','value',{ttl: 5_000,tags: ['temporary']});pad.set('session',token,{ttl: 30_000,slidingTtl: true});

Parameters:

  • key (string) -- The entry key.
  • value (T) -- The value to store.
  • options (EntryOptions, optional) -- Per-entry configuration.
OptionTypeDefaultDescription
ttlnumber | nullInherits defaultTtlTTL in milliseconds. null disables expiration for this entry.
slidingTtlbooleanInherits defaultSlidingTtlWhether TTL resets on each get() access.
tagsstring[][]String labels for categorizing the entry.

Returns:void

get<T>(key)

Retrieves the value for a key. If the entry has expired, it is removed, an expire event fires, and undefined is returned. On a successful read, accessedAt is updated (which resets the sliding TTL window if applicable).

constuser=pad.get<{id: number;name: string}>('user');

Parameters:

  • key (string) -- The entry key.

Returns:T | undefined

has(key)

Checks whether a key exists and is not expired. Expired entries are removed and trigger an expire event.

if(pad.has('apiResponse')){// entry is live}

Parameters:

  • key (string) -- The entry key.

Returns:boolean

delete(key)

Removes an entry by key. Fires a delete event if the entry existed.

constremoved=pad.delete('staleData');// true if it existed

Parameters:

  • key (string) -- The entry key.

Returns:boolean -- true if the entry existed and was removed, false otherwise.

clear()

Removes all entries from the scratchpad. Fires a clear event with the count of removed entries.

pad.clear();

Returns:void

keys()

Returns an array of all non-expired keys. Expired entries encountered during iteration are excluded.

constallKeys=pad.keys();// ['step', 'user', 'result']

Returns:string[]

entries()

Returns an array of [key, ScratchpadEntry] tuples for all non-expired entries.

for(const[key,entry]ofpad.entries()){console.log(key,entry.value,entry.tags);}

Returns:[string, ScratchpadEntry][]

findByTag(tag)

Returns all non-expired entries whose tags array includes the given tag (exact match).

pad.set('london','UK capital',{tags: ['geo','important']});pad.set('paris','France capital',{tags: ['geo']});constgeoEntries=pad.findByTag('geo');// both entries

Parameters:

  • tag (string) -- The tag to search for.

Returns:ScratchpadEntry[]

namespace(name)

Returns a scoped view of the scratchpad where all operations are prefixed with name:. Namespaces share the underlying storage with the parent -- they are views, not copies. Namespaces can be nested.

constmemory=pad.namespace('memory');memory.set('fact','The sky is blue');memory.get('fact');// 'The sky is blue'pad.get('memory:fact');// 'The sky is blue'// Nested namespaces compose prefixesconstdeep=pad.namespace('a').namespace('b');deep.set('key','val');pad.get('a:b:key');// 'val'// Namespace-scoped operationsmemory.keys();// ['fact'] (prefix stripped)memory.clear();// removes only memory:* entries

Parameters:

  • name (string) -- The namespace prefix.

Returns:Scratchpad -- A namespace-scoped scratchpad instance with the same full API.

snapshot()

Captures the full scratchpad state at the current point in time. The returned snapshot is a plain object suitable for serialization.

constsnap=pad.snapshot();// { entries: { ... }, timestamp: 1710000000000, version: 1 }

Returns:ScratchpadSnapshot

restore(snapshot)

Replaces the entire scratchpad state with the contents of a snapshot. Clears all existing entries before restoring. Throws ScratchpadVersionError if the snapshot version is not supported.

pad.restore(snap);

Parameters:

  • snapshot (ScratchpadSnapshot) -- A snapshot previously obtained from snapshot() or serialize().

Returns:void

Throws:ScratchpadVersionError if snapshot.version is not 1.

serialize()

Alias for snapshot(). Returns the same ScratchpadSnapshot structure.

constdata=pad.serialize();

Returns:ScratchpadSnapshot

toContext(options?)

Renders scratchpad contents as a formatted string suitable for injection into an LLM prompt. Supports filtering by tags or namespace, multiple output formats, token budget limits, and custom headers.

pad.set('name','Alice');pad.set('role','admin');pad.toContext();// 'name: Alice\nrole: admin'pad.toContext({format: 'markdown'});// '## name\nAlice\n\n## role\nadmin'pad.toContext({format: 'json'});// '{"name":"Alice","role":"admin"}'pad.toContext({format: 'xml'});// '<entry key="name">Alice</entry>\n<entry key="role">admin</entry>'

Parameters:

OptionTypeDefaultDescription
format'kv' | 'markdown' | 'xml' | 'json''kv'Output format.
filterTagsstring[]undefinedOnly include entries that have at least one of the specified tags.
filterNamespacestringundefinedOnly include entries whose key starts with the given namespace prefix.
maxTokensnumberundefinedTruncate output to fit within this token budget.
tokenCounter(text: string) => numbertext.lengthFunction to count tokens. Used with maxTokens.
includeMetadatabooleanundefinedReserved for future use.
headerstringundefinedText prepended to the output before the formatted entries.

Returns:string

stats()

Returns aggregate statistics about the scratchpad's current state.

constst=pad.stats();// {// size: 3, // live (non-expired) entry count// rawSize: 4, // total entries including expired-not-yet-swept// namespaceCount: 2,// namespaces: ['ctx', 'mem'],// entriesWithTtl: 1,// tagCounts: { geo: 2, important: 1 },// oldestEntryAt: 1710000000000,// newestEntryAt: 1710000001000,// }

Returns:ScratchpadStats

FieldTypeDescription
sizenumberCount of non-expired entries.
rawSizenumberTotal entries in the store, including expired entries not yet swept.
namespaceCountnumberNumber of distinct namespace prefixes.
namespacesstring[]List of distinct namespace prefixes.
entriesWithTtlnumberCount of entries that have a TTL set.
tagCountsRecord<string, number>Count of entries per tag.
oldestEntryAtnumber | nullcreatedAt of the oldest live entry, or null if empty.
newestEntryAtnumber | nullcreatedAt of the newest live entry, or null if empty.

on(event, handler)

Registers an event handler. Returns an unsubscribe function.

constunsub=pad.on('set',({ key, entry, isUpdate })=>{console.log(isUpdate ? 'updated' : 'created',key);});pad.on('delete',({ key, entry })=>{console.log('deleted',key);});pad.on('expire',({ key, entry })=>{console.log('expired',key);});pad.on('clear',({ count })=>{console.log('cleared',count,'entries');});// Stop listeningunsub();

Parameters:

  • event (ScratchpadEventName) -- One of 'set', 'delete', 'expire', 'clear'.
  • handler (ScratchpadEventHandler<K>) -- Callback receiving the event payload.

Event Payloads:

EventPayload
set{ key: string; entry: ScratchpadEntry; isUpdate: boolean }
delete{ key: string; entry: ScratchpadEntry }
expire{ key: string; entry: ScratchpadEntry }
clear{ count: number }

Returns:() => void -- Call to unsubscribe.

save()

Persists the current scratchpad state using the configured PersistenceAdapter. No-op if no adapter was provided.

awaitpad.save();

Returns:Promise<void>

load()

Loads scratchpad state from the configured PersistenceAdapter and restores it. No-op if no adapter was provided or the adapter returns null.

awaitpad.load();

Returns:Promise<void>

destroy()

Cleans up resources. Stops the background sweep timer if one is running.

awaitpad.destroy();

Returns:Promise<void>


TTL Utility Functions

isExpired(entry, now)

Determines whether a scratchpad entry has expired based on its TTL configuration.

import{isExpired}from'agent-scratchpad';constexpired=isExpired(entry,Date.now());

Parameters:

  • entry (ScratchpadEntry) -- The entry to check.
  • now (number) -- Current timestamp in milliseconds.

Returns:boolean -- true if the entry's TTL has elapsed.

Logic:

  • Returns false if entry.ttl is null.
  • For fixed TTL (slidingTtl: false): expired when now >= entry.createdAt + entry.ttl.
  • For sliding TTL (slidingTtl: true): expired when now >= entry.accessedAt + entry.ttl.

expiresAt(entry)

Calculates the absolute expiration timestamp for an entry.

import{expiresAt}from'agent-scratchpad';constexpiry=expiresAt(entry);// number | null

Parameters:

  • entry (ScratchpadEntry) -- The entry to inspect.

Returns:number | null -- The Unix timestamp (ms) when the entry expires, or null if it has no TTL.


Types

ScratchpadEntry<T>

interfaceScratchpadEntry<T=unknown>{key: string;value: T;createdAt: number;// Unix ms when first createdupdatedAt: number;// Unix ms when value last updatedaccessedAt: number;// Unix ms when last read via get()ttl: number|null;// TTL in ms, null = no expirationslidingTtl: boolean;// true = TTL resets on accesstags: string[];// string labels for categorization}

EntryOptions

interfaceEntryOptions{ttl?: number|null;slidingTtl?: boolean;tags?: string[];}

ScratchpadOptions

interfaceScratchpadOptions{defaultTtl?: number|null;defaultSlidingTtl?: boolean;sweepIntervalMs?: number|null;now?: ()=>number;persistence?: PersistenceAdapter;}

ScratchpadSnapshot

interfaceScratchpadSnapshot{entries: Record<string,ScratchpadEntry>;timestamp: number;version: 1;}

ScratchpadStats

interfaceScratchpadStats{size: number;rawSize: number;namespaceCount: number;namespaces: string[];entriesWithTtl: number;tagCounts: Record<string,number>;oldestEntryAt: number|null;newestEntryAt: number|null;}

ToContextOptions

interfaceToContextOptions{format?: 'markdown'|'xml'|'json'|'kv';filterTags?: string[];filterNamespace?: string;maxTokens?: number;tokenCounter?: (text: string)=>number;includeMetadata?: boolean;header?: string;}

PersistenceAdapter

interfacePersistenceAdapter{load(): Promise<ScratchpadSnapshot|null>;save(snap: ScratchpadSnapshot): Promise<void>;}

ScratchpadEvents

interfaceScratchpadEvents{set: {key: string;entry: ScratchpadEntry;isUpdate: boolean};delete: {key: string;entry: ScratchpadEntry};expire: {key: string;entry: ScratchpadEntry};clear: {count: number};}

ScratchpadEventName

typeScratchpadEventName='set'|'delete'|'expire'|'clear';

ScratchpadEventHandler<K>

typeScratchpadEventHandler<KextendsScratchpadEventName>=(data: ScratchpadEvents[K])=>void;

Error Handling

agent-scratchpad exports three error classes, all extending a common base.

ScratchpadError

Base class for all scratchpad errors. Extends Error with a code property.

import{ScratchpadError}from'agent-scratchpad';try{pad.restore(badSnapshot);}catch(err){if(errinstanceofScratchpadError){console.error(err.code);// e.g. 'SCRATCHPAD_VERSION_ERROR'console.error(err.message);// human-readable description}}
PropertyTypeDescription
codestringMachine-readable error code.
messagestringHuman-readable error description.
namestringAlways 'ScratchpadError'.

ScratchpadConfigError

Thrown when invalid configuration is provided to createScratchpad().

  • Code:SCRATCHPAD_CONFIG_ERROR

ScratchpadVersionError

Thrown when restore() encounters a snapshot with an unsupported version number.

  • Code:SCRATCHPAD_VERSION_ERROR
  • Additional property:version (number) -- The unsupported version that was encountered.
import{ScratchpadVersionError}from'agent-scratchpad';try{pad.restore(snap);}catch(err){if(errinstanceofScratchpadVersionError){console.error(`Unsupported version: ${err.version}`);}}

Advanced Usage

Agent Working Memory in a ReAct Loop

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad({defaultTtl: 300_000});// 5-minute default// Step 1: Store tool outputpad.set('search:result',apiResponse,{tags: ['tool-result','search']});// Step 2: Extract and store entitiespad.set('entities:user',{name: 'Alice',id: 42},{tags: ['entity']});pad.set('entities:order',{orderId: '#12345'},{tags: ['entity']});// Step 3: Inject scratchpad into promptconstagentContext=pad.toContext({format: 'markdown',header: '## Agent Working Memory',filterTags: ['entity'],});// Produces:// ## Agent Working Memory// ## entities:user// [object Object]// ...

Namespace Isolation for Multi-Agent Systems

constpad=createScratchpad();constagent1=pad.namespace('agent1');constagent2=pad.namespace('agent2');agent1.set('plan','Research the topic');agent2.set('plan','Draft the response');// Each agent sees only its own entriesagent1.keys();// ['plan']agent2.keys();// ['plan']// Parent sees all entries with prefixed keyspad.keys();// ['agent1:plan', 'agent2:plan']// Clear one agent without affecting the otheragent1.clear();agent2.keys();// ['plan'] -- unaffected

Sliding TTL for Session-Like Data

constpad=createScratchpad();// Session token stays alive as long as the agent keeps accessing itpad.set('session',{token: 'abc123'},{ttl: 30_000,slidingTtl: true});// Each access resets the 30-second expiration windowpad.get('session');// resets timerpad.get('session');// resets timer again// If 30 seconds pass without access, the entry expires

Background Sweep with Event Logging

constpad=createScratchpad({sweepIntervalMs: 10_000});pad.on('expire',({ key, entry })=>{console.log(`Expired: ${key} (created ${newDate(entry.createdAt).toISOString()})`);});pad.set('temp','data',{ttl: 15_000});// The sweep timer runs every 10 seconds and removes expired entries.// The expire event fires for each entry removed by the sweep.// Stop the sweep timer when doneawaitpad.destroy();

Persistence with a File-Based Adapter

import{createScratchpad,PersistenceAdapter,ScratchpadSnapshot}from'agent-scratchpad';importfsfrom'fs/promises';constfileAdapter: PersistenceAdapter={asyncload(){try{constdata=awaitfs.readFile('scratchpad.json','utf8');returnJSON.parse(data)asScratchpadSnapshot;}catch{returnnull;}},asyncsave(snap){awaitfs.writeFile('scratchpad.json',JSON.stringify(snap,null,2));},};constpad=createScratchpad({persistence: fileAdapter});// Restore previous state on startupawaitpad.load();// Work with the scratchpadpad.set('progress','step-3');// Persist state before shutdownawaitpad.save();

Snapshot-Based Backtracking

constpad=createScratchpad();pad.set('approach','strategy-A');pad.set('findings',['result-1']);// Save state before trying something riskyconstcheckpoint=pad.snapshot();pad.set('approach','strategy-B');pad.set('findings',['result-1','result-2-failed']);// Strategy B failed -- roll backpad.restore(checkpoint);pad.get('approach');// 'strategy-A'

Token-Budget-Aware Context Rendering

import{createScratchpad}from'agent-scratchpad';constpad=createScratchpad();pad.set('summary','A long summary of findings...');pad.set('details','Extensive details that may not fit...');// Use a custom token counter (e.g., tiktoken)constcontext=pad.toContext({format: 'kv',maxTokens: 500,tokenCounter: (text)=>Math.ceil(text.length/4),// rough estimateheader: '## Working Memory',});

Deterministic Testing with Custom Time

import{createScratchpad}from'agent-scratchpad';letnow=0;constpad=createScratchpad({now: ()=>now});pad.set('key','value',{ttl: 100});now=50;pad.get('key');// 'value' -- still alivenow=100;pad.get('key');// undefined -- expired exactly at 100ms

TypeScript

agent-scratchpad is written in TypeScript and ships with full type declarations. All exported functions, interfaces, and types are available for import:

import{createScratchpad,fromSnapshot,toContext,isExpired,expiresAt,ScratchpadError,ScratchpadConfigError,ScratchpadVersionError,}from'agent-scratchpad';importtype{Scratchpad,ScratchpadEntry,ScratchpadOptions,ScratchpadSnapshot,ScratchpadStats,ToContextOptions,EntryOptions,ScratchpadEventName,ScratchpadEventHandler,ScratchpadEvents,PersistenceAdapter,}from'agent-scratchpad';

Generic type parameters on get<T>() and set<T>() provide type-safe value access without casts:

interfaceUser{id: number;name: string;}pad.set<User>('user',{id: 1,name: 'Alice'});constuser=pad.get<User>('user');// user is User | undefined

License

MIT

About

Lightweight key-value scratchpad for agent reasoning

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages