NOETIC
Framework
Context Layers

Decisions History

A context layer that records every decide step's answers, so later steps can read them and later LLM calls can see them.

Overview

A decide call never writes to the conversation. decisionsHistoryLayer writes decisions back into the context system: it records each decision through the onDecision hook, exposes the records as data on ctx.context.decisionsHistory, and renders a one-line summary per decision into the view of later callModel steps.

  • Slot: 110 (Slot.WORKING_MEMORY + 10)
  • Default scope: 'execution'
  • Default maxRecords: 50 (oldest dropped first)
  • Hooks: init, recall, onDecision, onSpawn, onReturn

Usage

import { AgentHarness, decide, decisionsHistoryLayer, runCode } from '@noetic-tools/core';

const harness = new AgentHarness({
  name: 'support',
  params: {},
  contextLayers: [decisionsHistoryLayer()],
});

const triage = decide({
  id: 'triage',
  model: 'typesafe/jev-1.14',
  questions: {
    isUrgent: {
      type: 'boolean',
      instructions: 'Is the customer reporting downtime?',
    },
  },
});

const followUp = runCode({
  id: 'follow-up',
  execute: async (input: string, ctx) => {
    const record = ctx.context.decisionsHistory?.latest;
    return { input, record };
  },
});

The layer keeps its literal id, even inline in context([decisionsHistoryLayer()]), so ctx.context.decisionsHistory is typed and a misspelled key is a type error. Pass id to use a different key; the literal is kept there too.

Configuration

interface DecisionsHistoryConfig<Id extends string> {
  id?: Id;               // default 'decisionsHistory'
  scope?: ContextScope;  // default 'execution'
  maxRecords?: number;   // default 50, oldest dropped first
  recall?: boolean;      // render a summary into the view; default true
  slot?: number;         // default Slot.WORKING_MEMORY + 10
}
FieldTypeDefaultPurpose
idstring'decisionsHistory'Layer id, and so the ctx.context key
scopeContextScope'execution'Persistence boundary. With 'thread' or 'resource', history persists across runs
maxRecordsnumber50Records kept; the oldest drop first
recallbooleantrueRender a summary of the latest decisions into the view. Set false to keep records as data only
slotnumberSlot.WORKING_MEMORY + 10Recall order

Records

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

answers holds the step's answers as returned, refusals included.

Provides

KeyKindValue
latestdataReadonly<Record<string, DecisionRecord>> — the newest record per step id
alldataReadonlyArray<DecisionRecord> — every kept record, oldest first
const record = ctx.context.decisionsHistory.latest.triage;

Both entries are data, not functions, so the layer adds no LLM tools.

Recall Summary

When recall is on and at least one decision is recorded, the layer renders one line per step id, newest record only:

Decisions so far:
- triage: destination=billing (0.81), isUrgent=yes (0.92), urgency=medium (1.3)

Each answer renders as:

AnswerFormat
booleanyes or no, then the probability of yes
choicethe chosen value, then its probability
scorethe most probable level, then the weighted value
refusalrefused

Numbers are rounded to two places.

The onDecision Hook

The layer records decisions through onDecision, an optional hook any layer can implement. The runtime calls it after every decide step completes, in slot order, one layer at a time, so a later layer can read what an earlier one recorded. Return { state } to keep new state, or undefined to leave it as is.

interface OnDecisionParams<TState> {
  state: TState;
  record: DecisionRecord;
  ctx: ExecutionContext;
}

The hook has a 5s default timeout (timeouts.onDecision). A hook that throws or times out is logged as a diagnostic and does not fail the step. See Custom Layers to write your own.

Persistence and Resume

init loads the layer's saved state from storage, so with scope: 'thread' or 'resource' a later run starts with the earlier runs' records. When a harness resumes an execution, the layer keeps the restored state rather than starting empty. A decision replayed from the step ledger on resume does not fire onDecision again; its record comes back with the checkpoint.

Forks and Spawns

A inParallel path or a spawn child starts with a copy of the parent's records (onSpawn). When it returns, onReturn merges back only the records the child added, so concurrent paths keep each other's decisions.

On this page