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';
}| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique layer identifier. |
name | string | no | Human-readable name for debugging and trace output. |
slot | number | yes | Ordering slot (lower = recalled first) within the layer's band. Use the Slot constants for well-known positions. |
scope | ContextScope | yes | Persistence boundary. |
budget | BudgetConfig | no | Token budget allocation: a fixed number, a { min, max } range, or 'auto'. |
hooks | ContextLayerHooks<TState> | yes | Lifecycle callbacks. |
timeouts | Partial<LayerTimeouts> | no | Per-hook timeout overrides (ms). |
onInitError | 'throw' | 'disable' | no | What 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' | no | Whether 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. |
provides | LayerProvides | no | Typed data and function declarations exposed on ctx.context['layerId']. Function entries are also auto-injected as LLM tools. |
itemSchemas | Pick<ItemSchemaExtensions, 'developerMessages' | 'items'> | no | Optional item schemas contributed by this layer, primarily for developer-role context items. |
rerenderTiming | 'immediate' | 'batched' | no | Default re-render timing when onItemAppend requests a re-render. |
placement | LayerPlacement | no | Which 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';| Value | Description |
|---|---|
'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 }>;
}recallmay also return a barestringshorthand — the runtime wraps it in a developer message and estimates its token count — ornullto contribute nothing this turn.beforeToolCall/afterModelCallare the steering hooks: they return aSteeringDecision(allow / deny / guide) plus updated state. A deny surfaces as aNoeticErrorkindsteering_denied.onSteerdecides what happens to a steer (a message sent into the running turn) at each steer point:apply,defer, ordrop, with optional replacement items, andrecall: trueto 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.onItemAppendruns 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 (usestore).projectHistoryprojects (caps, transforms) the history portion of the context window once per callModel step. Read-side only —itemLogstorage is never mutated.renderDeltais called for an anchored layer whose pinned output has gone stale, to describe the change compactly instead of re-sending the whole block. Returnnullto fall back to the default, which republishes the full new content. Never called for layers in the'live'band.onDecisionis called after everydecidestep completes, in slot order, one layer at a time. Return{ state }to record the decision. A decide step runs nostore, 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
| Field | Type | Description |
|---|---|---|
storage | ScopedStorage | Scoped storage for this layer |
scopeKey | string | Resolved scope key |
ctx | ExecutionContext | Execution context |
InitResult
| Field | Type | Description |
|---|---|---|
state | TState | Initial layer state |
RecallParams
| Field | Type | Description |
|---|---|---|
log | ItemLog | Current conversation log |
query | string | Current user query |
ctx | ExecutionContext | Execution context |
state | TState | Current layer state |
budget | number | Allocated token budget |
RecallResult
| Field | Type | Description |
|---|---|---|
items | Item[] | Items to inject into the prompt |
tokenCount | number | Tokens consumed by the items |
state | TState | Updated 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;
}| Field | Type | Description |
|---|---|---|
peek | boolean | Render 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
| Field | Type | Description |
|---|---|---|
prev | ReadonlyArray<Item> | The items still pinned in the anchor band — what the model can currently see |
next | ReadonlyArray<Item> | The freshly recalled items that would have replaced them |
prevState | TState | undefined | The 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 |
state | TState | undefined | The layer's current state |
ctx | ExecutionContext | Execution context |
budget | number | Soft 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
| Field | Type | Description |
|---|---|---|
newItems | Item[] | New items from the LLM response |
log | ItemLog | Full conversation log |
response | LLMResponse | Complete LLM response |
ctx | ExecutionContext | Execution context |
state | TState | Current layer state |
StoreResult
| Field | Type | Description |
|---|---|---|
state | TState | Updated state |
SpawnParams
| Field | Type | Description |
|---|---|---|
parentState | TState | Parent layer state |
childCtx | ExecutionContext | Child execution context |
SpawnResult
| Field | Type | Description |
|---|---|---|
childState | TState | null | State for the child (null = the child gets no state; items still apply) |
items | Item[] | 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
| Field | Type | Description |
|---|---|---|
childState | TState | Child's final state |
childLog | ItemLog | Child's conversation log |
parentState | TState | Parent's current state |
result | unknown | Child's completion result |
childCtx | ExecutionContext | The child's execution context — the same one onSpawn received. Namespace merges by childCtx.executionId when several children return into one parent. |
ReturnResult
| Field | Type | Description |
|---|---|---|
parentState | TState | Updated parent state |
result | unknown | Transformed 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.
| Field | Type | Description |
|---|---|---|
parentState | TState | The state the parent just wrote |
childState | TState | The child's state when the hook starts |
childCtx | ExecutionContext | The child's execution context |
ParentUpdateResult
Return nothing to ignore the update.
| Field | Type | Description |
|---|---|---|
childState | TState | New child state (optional; written when the key is present, so undefined clears it) |
items | Item[] | Items appended to the child's log (optional) |
CompleteParams
| Field | Type | Description |
|---|---|---|
log | ItemLog | Full conversation log |
ctx | ExecutionContext | Execution context |
state | TState | Current layer state |
outcome | ExecutionOutcome | How the execution ended |
DisposeParams
| Field | Type | Description |
|---|---|---|
state | TState | Final layer state |
OnDecisionParams
| Field | Type | Description |
|---|---|---|
state | TState | Current layer state |
record | DecisionRecord | The completed decision |
ctx | ExecutionContext | Execution 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
| Field | Type | Description |
|---|---|---|
init | number | Timeout for init hook (ms, optional) |
recall | number | Timeout for recall hook (ms, optional) |
store | number | Timeout for store hook (ms, optional) |
onSpawn | number | Timeout for onSpawn hook (ms, optional) |
onReturn | number | Timeout for onReturn hook (ms, optional) |
onParentUpdate | number | Timeout for onParentUpdate hook (ms, optional, default 5000) |
onComplete | number | Timeout for onComplete hook (ms, optional) |
dispose | number | Timeout for dispose hook (ms, optional) |
beforeToolCall | number | Timeout for beforeToolCall steering hook (ms, optional) |
afterModelCall | number | Timeout for afterModelCall steering hook (ms, optional) |
onItemAppend | number | Timeout for onItemAppend hook (ms, optional) |
projectHistory | number | Timeout for projectHistory hook (ms, optional) |
onDecision | number | Timeout for onDecision hook (ms, optional, default 5000) |
onSteer | number | Timeout 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;
}| Field | Type | Required | Description |
|---|---|---|---|
tokenBudget | number | yes | Total token budget for all layers |
responseReserve | number | yes | Tokens reserved for the model response |
overflow | 'truncate' | 'summarize' | 'sliding_window' | yes | Strategy when total recall exceeds budget |
overflowModel | string | no | Model for summarization overflow |
windowSize | number | no | Items 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;
}| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Master 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. |
minCachedTokens | number | 100 | Re-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. |
minEpochAssemblies | number | 2 | Assemblies an epoch must reach before its cache figures are judged. The first assembly after a re-anchor writes the cache rather than reading it. |
maxEpochAssemblies | number | 50 | Assemblies after which an epoch re-anchors regardless of cache figures. |
deltaBudgetFraction | number | 0.15 | Re-anchor once supersedes cost more than this fraction of the anchor band. |
autoDemoteChurn | number | 0.5 | An 'auto' layer changing at least this often moves to the live band. |
autoPromoteChurn | number | 0.2 | An 'auto' layer changing at most this often moves back to the anchor band. |
minChurnSamples | number | 3 | Assemblies a layer must be watched for before its placement moves. |
churnDecay | number | 0.5 | Fraction 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[]>;
}| Method | Description |
|---|---|
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;
}| Method | Description |
|---|---|
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>;
}| Field | Type | Description |
|---|---|---|
layers | TLayers | Array of context layers |
_shape | InferContextShape<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;
}| Field | Type | Description |
|---|---|---|
kind | 'data' | Discriminant |
read | (state: TState) => T | Derives 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 }>;
}| Field | Type | Description |
|---|---|---|
kind | 'function' | Discriminant |
description | string | Human-readable description for the LLM |
input | ZodType<TInput> | Zod schema for input validation |
output | ZodType<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.