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>;
  onParentUpdate?: (params: ParentUpdateParams<TState>) => Promise<ParentUpdateResult<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>>;
  onSteer?: (params: OnSteerParams<TState>) => Promise<OnSteerResult<TState>>;
  onItemAppend?: (params: OnItemAppendParams<TState>) => Promise<OnItemAppendResult<TState>>;
  projectHistory?: (params: ProjectHistoryParams<TState>) => Promise<ProjectHistoryResult>;
  renderDelta?: (params: RenderDeltaParams<TState>) => Promise<string | null>;
  onDecision?: (params: OnDecisionParams<TState>) => Promise<undefined | { state: TState }>;
}
  • 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.
  • onSteer decides what happens to a steer (a message sent into the running turn) at each steer point: apply, defer, or drop, with optional replacement items, and recall: true to run recall again once it is applied. Layers run in slot order, each seeing the previous layer's action and items. See Steering a running turn.
  • 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.
  • onDecision is called after every decide step completes, in slot order, one layer at a time. Return { state } to record the decision. A decide step runs no store, so this is how a layer learns about decisions. A hook that throws or times out is logged as a diagnostic and does not fail the step. A decision replayed from the step ledger on resume does not call it again.

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)

RecallOptions

Options a caller passes to the harness's recallLayersAtomic / recallLayersEventual. The layer's recall hook does not receive them.

interface RecallOptions {
  peek?: boolean;
}
FieldTypeDescription
peekbooleanRender without leaving a trace: each recall gets a deep copy of its state, state it returns is not kept, and eventual-layer output is not cached. A layer whose state cannot be copied (it holds functions) is skipped. A decide step peeks so it never drains state (such as queued steering guidance) meant for the next model call

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 = the child gets no state; items still apply)
itemsItem[]Additional items for the child (optional)

onSpawn and onReturn run one layer at a time in slot order (ties by array order), so the onReturn result pipeline runs from the lowest slot to the highest.

Return null instead of a SpawnResult to disable the layer in the child: none of its hooks run for the child's execution. A hook that throws or times out disables the layer the same way and is logged as a diagnostic.

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)

ParentUpdateParams

onParentUpdate runs in a live child (a spawn child or an inParallel path) each time the parent writes this layer's state. It runs in the background: the parent's write never waits for it or fails because of it. Each child takes its updates one at a time, in write order, and stops taking them before its onReturn runs. A hook that throws or times out is logged as a diagnostic and the update is dropped. See Spawn.

FieldTypeDescription
parentStateTStateThe state the parent just wrote
childStateTStateThe child's state when the hook starts
childCtxExecutionContextThe child's execution context

ParentUpdateResult

Return nothing to ignore the update.

FieldTypeDescription
childStateTStateNew child state (optional; written when the key is present, so undefined clears it)
itemsItem[]Items appended to the child's log (optional)

CompleteParams

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

DisposeParams

FieldTypeDescription
stateTStateFinal layer state

OnDecisionParams

FieldTypeDescription
stateTStateCurrent layer state
recordDecisionRecordThe completed decision
ctxExecutionContextExecution context

DecisionRecord

interface DecisionRecord {
  stepId: string;
  at: number;                     // epoch ms
  modelId: string;
  source: 'native' | 'estimated';
  answers: Readonly<Record<string, DecideAnswer>>;
}

DecisionsHistoryLayer

DecisionsHistoryLayer<Id> is what decisionsHistoryLayer() returns: a layer whose id is the literal Id (default 'decisionsHistory'), with state DecisionsHistoryState ({ records: DecisionRecord[] }) and provides entries latest and all. The literal id is what types ctx.context.decisionsHistory.

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;
    createSpan?(name: string): ExecutionSpan;
  };
  readLayerState<T>(layerId: string): T | undefined;
}

interface ExecutionSpan {
  setAttribute(key: string, value: string | number | boolean): void;
  addEvent(name: string, attributes?: Record<string, string | number | boolean>): void;
  end(): Promise<void>; // ends the span and exports it
}

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. trace annotates the span of the context the hook runs for; createSpan (present on every context the runtime builds) opens a child of that span, exported to the harness traceExporter when end() resolves.

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)
onParentUpdatenumberTimeout for onParentUpdate hook (ms, optional, default 5000)
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)
onDecisionnumberTimeout for onDecision hook (ms, optional, default 5000)
onSteernumberTimeout for onSteer hook (ms, optional, default 5000)

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', or whose onSpawn returned null or failed for a child execution

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, recorded as a layer.hook child span of the context's span (or, when the ExecutionContext has no trace.createSpan, a layer.hook span event, NoeticEvent.LAYER_HOOK). See Observability.

interface LayerTraceSpan {
  layerId: string;
  hook:
    | 'init' | 'recall' | 'store' | 'onSpawn' | 'onReturn' | 'onParentUpdate' | 'onComplete'
    | 'dispose' | 'onDecision' | 'beforeToolCall' | 'afterModelCall' | 'projectHistory' | 'onItemAppend'
    | 'onSteer';
  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