NOETIC
Framework
API Reference

Context Layer Types

Type definitions for context layers, hooks, storage adapters, and projection policies in Noetic.

ContextLayer

The core context layer interface.

declare interface ContextLayerShape<TState = unknown> {
  id: string;
  name?: string;
  slot: number;
  scope: ContextScope;
  budget?: BudgetConfig;
  hooks: ContextLayerHooks<TState>;
  timeouts?: Partial<LayerTimeouts>;
  onInitError?: 'throw' | 'disable';
  recallMode?: 'atomic' | 'eventual';
  provides?: unknown;
  itemSchemas?: unknown;
  rerenderTiming?: 'immediate' | 'batched';
  placement?: 'anchor' | 'live' | 'auto';
}
FieldTypeRequiredDescription
idstringyesUnique layer identifier.
namestringnoHuman-readable name for debugging and trace output.
slotnumberyesOrdering slot (lower = recalled first) within the layer's band. Use the Slot constants for well-known positions.
scopeContextScopeyesPersistence boundary.
budgetBudgetConfignoToken budget allocation: a fixed number, a { min, max } range, or 'auto'.
hooksContextLayerHooks<TState>yesLifecycle callbacks.
timeoutsPartial<LayerTimeouts>noPer-hook timeout overrides (ms).
onInitError'throw' | 'disable'noWhat to do when init throws. 'throw' (default) surfaces the error and aborts the execution; 'disable' logs a diagnostic and runs the execution without this layer. Opt into 'disable' only for non-critical layers.
recallMode'atomic' | 'eventual'noWhether recall() blocks the model call. 'atomic' (default) runs in the hot path; 'eventual' serves recall from cache and refreshes after store(), so the next turn sees the update. A harness configured with forceAtomicRecall treats every layer as atomic.
providesLayerProvidesnoTyped data and function declarations exposed on ctx.context['layerId']. Function entries are also auto-injected as LLM tools.
itemSchemasPick<ItemSchemaExtensions, 'developerMessages' | 'items'>noOptional item schemas contributed by this layer, primarily for developer-role context items.
rerenderTiming'immediate' | 'batched'noDefault re-render timing when onItemAppend requests a re-render.
placementLayerPlacementnoWhich band of the assembled view the layer renders into, and so whether it is pinned for the prompt cache. Defaults to 'auto'.

LayerPlacement

type LayerPlacement = 'anchor' | 'live' | 'auto';
ValueDescription
'anchor'Renders before history and is pinned for the epoch: the bytes sent on the first assembly are re-sent unchanged until the next re-anchor, and any change is published as a supersede rather than by rewriting the prefix.
'live'Renders after history and is re-rendered freely. Use for content that changes every turn, or whose recall() mutates state and so cannot be replayed.
'auto'Default. Starts anchored; the runtime may move it to 'live' at an epoch boundary once it has watched how often the layer changes. An explicit 'anchor' or 'live' is never overridden.

A layer whose recall() returns new state is forced live whatever this field says — see Prompt Cache Anchoring.

ContextScope

type ContextScope = 'thread' | 'resource' | 'global' | 'execution';

BudgetConfig

type BudgetConfig =
  | number
  | { min: number; max: number }
  | 'auto';

An omitted budget behaves like 'auto': the layer has infinite headroom and splits the proportional pool with the other auto layers after finite layers take their share.

Slot Constants

const SlotShape = {
  REMINDER: 80,
  STEERING: 90,
  SCRATCHPAD: 100,
  ENTITY: 150,
  OBSERVATIONS: 200,
  PROCEDURAL: 250,
  EPISODIC: 300,
  RAG: 350,
  SEMANTIC_RECALL: 400,
} as const satisfies Record<string, number>;

The runtime exports the Slot constant from @noetic-tools/core with these values; pass any of them as a layer's slot field to position it consistently with built-in layers.

ContextLayerHooks

interface ContextLayerHooks<TState = unknown> {
  init?: (params: InitParams) => Promise<InitResult<TState>>;
  recall?: (params: RecallParams<TState>) => Promise<RecallResult<TState> | string | null>;
  store?: (params: StoreParams<TState>) => Promise<StoreResult<TState> | undefined>;
  onSpawn?: (params: SpawnParams<TState>) => Promise<SpawnResult<TState> | null>;
  onReturn?: (params: ReturnParams<TState>) => Promise<ReturnResult<TState> | undefined>;
  onComplete?: (params: CompleteParams<TState>) => Promise<void | { state: TState }>;
  dispose?: (params: DisposeParams<TState>) => Promise<void>;
  beforeToolCall?: (params: BeforeToolCallParams<TState>) => Promise<BeforeToolCallResult<TState>>;
  afterModelCall?: (params: AfterModelCallParams<TState>) => Promise<AfterModelCallResult<TState>>;
  onItemAppend?: (params: OnItemAppendParams<TState>) => Promise<OnItemAppendResult<TState>>;
  projectHistory?: (params: ProjectHistoryParams<TState>) => Promise<ProjectHistoryResult>;
  renderDelta?: (params: RenderDeltaParams<TState>) => Promise<string | null>;
}
  • recall may also return a bare string shorthand — the runtime wraps it in a developer message and estimates its token count — or null to contribute nothing this turn.
  • beforeToolCall / afterModelCall are the steering hooks: they return a SteeringDecision (allow / deny / guide) plus updated state. A deny surfaces as a NoeticError kind steering_denied.
  • onItemAppend runs when input items (user messages, tool outputs) are about to be appended — layers compose in slot order, each receiving the previous layer's output. It can transform, drop, or inject items, and may request a context re-render. Not called for LLM response items (use store).
  • projectHistory projects (caps, transforms) the history portion of the context window once per callModel step. Read-side only — itemLog storage is never mutated.
  • renderDelta is called for an anchored layer whose pinned output has gone stale, to describe the change compactly instead of re-sending the whole block. Return null to fall back to the default, which republishes the full new content. Never called for layers in the 'live' band.

Hook Parameter Types

InitParams

FieldTypeDescription
storageScopedStorageScoped storage for this layer
scopeKeystringResolved scope key
ctxExecutionContextExecution context

InitResult

FieldTypeDescription
stateTStateInitial layer state

RecallParams

FieldTypeDescription
logItemLogCurrent conversation log
querystringCurrent user query
ctxExecutionContextExecution context
stateTStateCurrent layer state
budgetnumberAllocated token budget

RecallResult

FieldTypeDescription
itemsItem[]Items to inject into the prompt
tokenCountnumberTokens consumed by the items
stateTStateUpdated state (optional)

RenderDeltaParams

FieldTypeDescription
prevReadonlyArray<Item>The items still pinned in the anchor band — what the model can currently see
nextReadonlyArray<Item>The freshly recalled items that would have replaced them
prevStateTState | undefinedThe layer state captured when prev was pinned. Best-effort: it is held by reference, so a layer that mutates state in place sees the current object rather than a snapshot
stateTState | undefinedThe layer's current state
ctxExecutionContextExecution context
budgetnumberSoft token budget for the returned text

Return the text to publish, or null to let the runtime republish the full new content. The hook is bounded by the layer's recall timeout (5s by default); a hook that throws or hangs falls back to the default, because a supersede is a correctness obligation and is never skipped.

StoreParams

FieldTypeDescription
newItemsItem[]New items from the LLM response
logItemLogFull conversation log
responseLLMResponseComplete LLM response
ctxExecutionContextExecution context
stateTStateCurrent layer state

StoreResult

FieldTypeDescription
stateTStateUpdated state

SpawnParams

FieldTypeDescription
parentStateTStateParent layer state
childCtxExecutionContextChild execution context

SpawnResult

FieldTypeDescription
childStateTState | nullState for the child (null = don't propagate)
itemsItem[]Additional items for the child (optional)

ReturnParams

FieldTypeDescription
childStateTStateChild's final state
childLogItemLogChild's conversation log
parentStateTStateParent's current state
resultunknownChild's completion result
childCtxExecutionContextThe child's execution context — the same one onSpawn received. Namespace merges by childCtx.executionId when several children return into one parent.

ReturnResult

FieldTypeDescription
parentStateTStateUpdated parent state
resultunknownTransformed result for pipeline (optional)

CompleteParams

FieldTypeDescription
logItemLogFull conversation log
ctxExecutionContextExecution context
stateTStateCurrent layer state
outcomeExecutionOutcomeHow the execution ended

DisposeParams

FieldTypeDescription
stateTStateFinal layer state

ExecutionOutcome

type ExecutionOutcome = 'success' | 'failure' | 'aborted';

ExecutionContext

Context available to context layer hooks (different from the step Context).

interface ExecutionContext {
  executionId: string;
  threadId: string;
  resourceId?: string;
  depth: number;
  stepNumber: number;
  tokenUsage: { input: number; output: number };
  cost: number;
  fs: FsAdapter;
  shell: ShellAdapter;
  callModel?: (request: LayerCallModelRequest) => Promise<LLMResponse>;
  tokenize(text: string): number;
  trace: {
    setAttribute(key: string, value: string | number | boolean): void;
    addEvent(name: string, attributes?: Record<string, string | number | boolean>): void;
  };
  readLayerState<T>(layerId: string): T | undefined;
}

fs is the FsAdapter from ctx.harness.fs, giving context layer hooks access to the same filesystem backend as tools and skill discovery (real, in-memory, or remote). shell is the corresponding ShellAdapter. callModel is the minimal model-call surface ({ model, items, instructions? }) — present only when the harness has an LLM provider configured, so LLM-backed layers must handle its absence. readLayerState snapshots a sibling layer's state by its layer.id.

LayerTimeouts

FieldTypeDescription
initnumberTimeout for init hook (ms, optional)
recallnumberTimeout for recall hook (ms, optional)
storenumberTimeout for store hook (ms, optional)
onSpawnnumberTimeout for onSpawn hook (ms, optional)
onReturnnumberTimeout for onReturn hook (ms, optional)
onCompletenumberTimeout for onComplete hook (ms, optional)
disposenumberTimeout for dispose hook (ms, optional)
beforeToolCallnumberTimeout for beforeToolCall steering hook (ms, optional)
afterModelCallnumberTimeout for afterModelCall steering hook (ms, optional)
onItemAppendnumberTimeout for onItemAppend hook (ms, optional)
projectHistorynumberTimeout for projectHistory hook (ms, optional)

renderDelta has no entry of its own — it borrows the recall timeout, defaulting to 5s.

ProjectionPolicy

interface ProjectionPolicy {
  tokenBudget: number;
  responseReserve: number;
  overflow: 'truncate' | 'summarize' | 'sliding_window';
  overflowModel?: string;
  windowSize?: number;
}
FieldTypeRequiredDescription
tokenBudgetnumberyesTotal token budget for all layers
responseReservenumberyesTokens reserved for the model response
overflow'truncate' | 'summarize' | 'sliding_window'yesStrategy when total recall exceeds budget
overflowModelstringnoModel for summarization overflow
windowSizenumbernoItems to keep for sliding window overflow

ContextCacheConfig

Tuning for prompt-cache anchoring, passed as contextCache on AgentConfig / the AgentHarness constructor. Every field is optional; the defaults give a layer set with no explicit placements a stable prefix without any configuration.

interface ContextCacheConfig {
  enabled?: boolean;
  minCachedTokens?: number;
  minEpochAssemblies?: number;
  maxEpochAssemblies?: number;
  deltaBudgetFraction?: number;
  autoDemoteChurn?: number;
  autoPromoteChurn?: number;
  minChurnSamples?: number;
  churnDecay?: number;
}
FieldTypeDefaultDescription
enabledbooleantrueMaster switch over both halves of anchoring. When off, every layer renders before history as it did before bands existed, and the cache_control: { type: 'ephemeral' } breakpoint is no longer sent — so Claude models, whose caching is opt-in, stop caching at all. A cache write costs more than a read, which is why the breakpoint follows the banding rather than going out on its own. See Asking for the cache.
minCachedTokensnumber100Re-anchor when the first round reports fewer cached tokens than this. Judged against what there was to cache, so a short prompt is not held to a floor it can never reach.
minEpochAssembliesnumber2Assemblies an epoch must reach before its cache figures are judged. The first assembly after a re-anchor writes the cache rather than reading it.
maxEpochAssembliesnumber50Assemblies after which an epoch re-anchors regardless of cache figures.
deltaBudgetFractionnumber0.15Re-anchor once supersedes cost more than this fraction of the anchor band.
autoDemoteChurnnumber0.5An 'auto' layer changing at least this often moves to the live band.
autoPromoteChurnnumber0.2An 'auto' layer changing at most this often moves back to the anchor band.
minChurnSamplesnumber3Assemblies a layer must be watched for before its placement moves.
churnDecaynumber0.5Fraction of the churn counters carried across a re-anchor.

ReanchorReason

type ReanchorReason =
  | 'cold-start'
  | 'instructions-changed'
  | 'cache-miss'
  | 'delta-pressure'
  | 'delta-overflow'
  | 'max-age';

Why an epoch dropped its pins and anchored again. Surfaced on EpochUsage.reanchorReason for the assembly that re-anchored. See Prompt Cache Anchoring for what triggers each one.

StorageAdapter

interface StorageAdapter {
  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T): Promise<void>;
  delete(key: string): Promise<void>;
  list(prefix: string): Promise<string[]>;
}
MethodDescription
get(key)Retrieve a value by key (returns null if not found)
set(key, value)Store a value
delete(key)Delete a value
list(prefix)List all keys with the given prefix

ScopedStorage

Same interface as StorageAdapter but scoped to a specific layer and scope key.

interface ScopedStorage {
  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T): Promise<void>;
  delete(key: string): Promise<void>;
  list(prefix?: string): Promise<string[]>;
}

LayerStateStore

In-context layer-state store with centralized durable write-through. Every set() for a registered (execution, layer) pair is asynchronously mirrored to the layer's ScopedStorage — state written by provides functions, lifecycle hooks, or store() all persists the same way. Available from @noetic-tools/core/unstable.

interface LayerStateStore {
  get<T>(executionId: string, layerId: string): T | undefined;
  set<T>(executionId: string, layerId: string, state: T): void;
  cleanup(executionId: string): void;
  diagnostic: (layerId: string, hook: string, error: unknown) => void;
  // Optional — implemented by createLayerStateStore(); custom stores may omit them.
  has?(executionId: string, layerId: string): boolean;
  registerDurable?(executionId: string, layerId: string, target: ScopedStorage): void;
  flush?(executionId: string): Promise<void>;
  disable?(executionId: string, layerId: string): void;
  isDisabled?(executionId: string, layerId: string): boolean;
}
MethodDescription
get(executionId, layerId)Read a layer's current in-memory state
set(executionId, layerId, state)Write state; mirrored durably when a target is registered (undefined deletes the durable key)
cleanup(executionId)Drop all state and registrations for an execution
diagnostic(layerId, hook, error)Sink for non-fatal layer errors (mirror failures report hook 'persist')
has(executionId, layerId)Whether any state entry exists — distinguishes "init never ran" (no entry → init-bearing layer skipped) from "state explicitly cleared" (entry present → hooks keep running with undefined state)
registerDurable(executionId, layerId, target)Register the durable write-through target for a layer (non-'execution' scopes; called by initLayers/spawnLayers)
flush(executionId)Await all in-flight durable mirror writes
disable(executionId, layerId) / isDisabled(...)Explicit disabled tracking for layers whose init failed with onInitError: 'disable'

Writes are coalesced per key (latest wins, one write in flight) and mirror failures never throw — they surface only through diagnostic.

LayerTraceSpan

Observability data for a single layer hook execution.

interface LayerTraceSpan {
  layerId: string;
  hook: 'init' | 'recall' | 'store' | 'onSpawn' | 'onReturn' | 'onComplete' | 'dispose';
  duration: number;
  status: 'ok' | 'error' | 'timeout' | 'skipped';
  budget?: { allocated: number; used: number; yielded: number };
  itemCount?: number;
  error?: { message: string; stack?: string };
}

ContextData

The default context shape used when no typed context config is provided.

type ContextData = Readonly<Record<string, Record<string, unknown>>>;

ContextConfig

A typed context configuration that carries a phantom _shape for compile-time context access. Created via the context() builder.

interface ContextConfig<TLayers extends ContextLayer[] = ContextLayer[]> {
  layers: TLayers;
  _shape: InferContextShape<TLayers>;
}
FieldTypeDescription
layersTLayersArray of context layers
_shapeInferContextShape<TLayers>Phantom field for compile-time shape inference (never set at runtime)

InferContext

Extracts the context shape type from a ContextConfig.

type InferContext<T extends { readonly _shape: unknown }> = T['_shape'];

InferContextShape

A mapped type that collects each layer's provides declarations into a record keyed by layer id. Each key maps to the resolved data/function types declared by that layer.

LayerProvides

type LayerProvides = Record<string, LayerDataDecl | LayerFunctionDecl>;

A record of named declarations that a layer exposes on ctx.context[layerId].

LayerDataDecl

interface LayerDataDecl<T = unknown, TState = unknown> {
  kind: 'data';
  read: (state: TState) => T;
}
FieldTypeDescription
kind'data'Discriminant
read(state: TState) => TDerives the value from layer state

LayerFunctionDecl

interface LayerFunctionDecl<TInput = unknown, TOutput = unknown, TState = unknown> {
  kind: 'function';
  description: string;
  input: ZodType<TInput>;
  output: ZodType<TOutput>;
  execute(args: TInput, state: TState, ctx: ExecutionContext): Promise<{ result: TOutput; state?: TState }>;
}
FieldTypeDescription
kind'function'Discriminant
descriptionstringHuman-readable description for the LLM
inputZodType<TInput>Zod schema for input validation
outputZodType<TOutput>Zod schema for output validation
execute(args: TInput, state: TState, ctx: ExecutionContext) => Promise<{ result: TOutput; state?: TState }>Execution function. Returns the result plus an optional state update; omit state to leave the layer's state untouched.

Builder Functions

layerData

function layerData<T, TState>(opts: {
  read: (state: TState) => T;
}): LayerDataDecl<T, TState>;

Creates a LayerDataDecl that derives a value from layer state. Takes an options object with a single read function.

layerFunction

function layerFunction<TInput, TOutput, TState>(opts: {
  description: string;
  input: ZodType<TInput>;
  output: ZodType<TOutput>;
  execute: (
    args: TInput,
    state: TState,
    ctx: ExecutionContext,
  ) => Promise<{ result: TOutput; state?: TState }>;
}): LayerFunctionDecl<TInput, TOutput, TState>;

Creates a LayerFunctionDecl that exposes a callable function on the context object. execute returns { result, state? }; include state to persist an updated layer state.

context

function context<const T extends readonly ContextLayer[]>(layers: T): ContextConfig<T>;

Collects an array of layers into a ContextConfig with inferred _shape — e.g. context([scratchpad(), plan()]). Pass the result to StepSpawn.context or AgentConfig to get typed ctx.context access.

On this page