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>;
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>;
}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.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.
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) |
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 = don't propagate) |
items | Item[] | Additional items for the child (optional) |
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) |
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 |
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
| 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) |
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) |
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' |
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>;
}| 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.