NOETIC
Framework
API Reference

AgentHarness Types

Type definitions for the AgentHarnessContract interface, AgentConfig, and related types in Noetic.

AgentHarnessContract

The core execution engine interface. The exported AgentHarness class implements this contract. Use the contract type when you want to be agnostic about the concrete implementation (e.g., for tests or custom harnesses).

declare interface HarnessApi<TParams extends Record<string, unknown> = Record<string, unknown>> {
  readonly config: AgentConfig<TParams>;
  readonly fs: FsAdapter;
  readonly shell: ShellAdapter;
  readonly subprocess: SubprocessAdapter;
  readonly rootCwdState: { cwd: string; previousCwd?: string };

  callModel(request: CallModelRequest): Promise<LLMResponse>;
  decide(request: DecideRequest): Promise<DecideResponse>;
  execute(input: ExecuteInput, options?: ExecuteOptions): Promise<void>;
  getAgentResponse(scope?: SessionScope): Promise<HarnessResponse>;
  getItemStream(scope?: SessionScope): AsyncIterable<StreamingItem>;
  getTextStream(scope?: SessionScope): AsyncIterable<string>;
  getReasoningStream(scope?: SessionScope): AsyncIterable<string>;
  getFullStream(scope?: SessionScope): AsyncIterable<StreamEvent>;
  abort(scope?: SessionScope & CancelOptions): Promise<DrainReport | undefined>;
  getStatus(scope: ExecutionScope): ExecutionStatus;
  getStatus(scope?: SessionScope): HarnessStatus;
  getQueueSize(scope?: SessionScope): number;
  seedSessionHistory(threadId: string, items: ReadonlyArray<Item>): void;
  setRootCwd(nextCwd: string): void;

  run<I, O>(step: Step<ContextData, I, O>, input: I, ctx: Context): Promise<O>;
  detachedSpawn<I, O>(
    step: Step<ContextData, I, O>,
    input: I,
    parentCtx: Context,
    overrides?: { threadId?: string; resourceId?: string; cwdInit?: string },
  ): DetachedHandle<O>;
  createContext(opts?: {
    parent?: Context;
    items?: Item[];
    state?: unknown;
    threadId?: string;
    resourceId?: string;
    contextLayers?: ContextLayer[];
    cwdInit?: string;
  }): Context;

  send<T>(channel: Channel<T>, value: NoInfer<T>, ctx: Context): Promise<void>;
  recv<T>(channel: Channel<T>, ctx: Context, opts?: { timeout?: number }): Promise<T>;
  tryRecv<T>(channel: Channel<T>, ctx: Context): T | null;
  getChannelHandle<T>(channel: ExternalChannel<T>, executionId: string): ChannelHandle<T>;
  getChannelStream<T>(channel: ExternalChannel<T>, executionId: string): AsyncIterable<T>;

  initLayers(layers: ContextLayer[], ctx: Context, storage: StorageAdapter): Promise<void>;
  recallLayers(layers: ContextLayer[], input: string, ctx: Context): Promise<RecallLayerOutput[]>;
  recallLayersAtomic(
    layers: ContextLayer[],
    input: string,
    ctx: Context,
    budgets: Map<string, number>,
    opts?: RecallOptions,
  ): Promise<RecallLayerOutput[]>;
  recallLayersEventual(
    layers: ContextLayer[],
    input: string,
    ctx: Context,
    budgets: Map<string, number>,
    opts?: RecallOptions,
  ): Promise<RecallLayerOutput[]>;
  projectHistory(
    layers: ContextLayer[],
    items: ReadonlyArray<Item>,
    ctx: Context,
  ): Promise<ReadonlyArray<Item>>;
  runAppendPipeline(
    layers: ContextLayer[],
    items: Item[],
    ctx: Context,
  ): Promise<{ items: Item[]; rerenderRequests: RerenderRequest[] }>;
  executeRerender(
    requests: RerenderRequest[],
    layers: ContextLayer[],
    ctx: Context,
    budgets: Map<string, number>,
    query?: string,
  ): Promise<RecallLayerOutput[]>;
  previewRequestItems(scope?: SessionScope): Promise<ReadonlyArray<Item>>;
  storeLayers(layers: ContextLayer[], response: LLMResponse, ctx: Context): Promise<void>;
  disposeLayers(layers: ContextLayer[], ctx: Context): Promise<void>;
  getLayerState<T>(executionId: string, layerId: string): T | undefined;
  setLayerState<T>(executionId: string, layerId: string, state: T): void;

  beforeToolCall(
    layers: ContextLayer[],
    toolName: string,
    toolArgs: unknown,
    ctx: Context,
  ): Promise<unknown>;
  afterModelCall(layers: ContextLayer[], response: LLMResponse, ctx: Context): Promise<unknown>;
  notifyDecision(layers: ContextLayer[], record: DecisionRecord, ctx: Context): Promise<void>;

  checkpoint(ctx: Context): Promise<void>;
  restore(executionId: string): Promise<Context | null>;
  cancel(ctx: Context, options?: string | CancelOptions): Promise<DrainReport | undefined>;
  createSpan(name: string, parent: Span | null): Span;
}

declare interface RerenderRequest {
  layerId: string;
  slot: number;
  timing: 'immediate' | 'batched';
  scope: 'self' | 'slot-after' | 'all';
}

AgentHarnessContract Methods

MethodDescription
configThe AgentConfig<TParams> the harness was constructed with.
fsThe FsAdapter the harness was constructed with (defaults to createLocalFsAdapter()). All filesystem operations — CLI tools, skill discovery, and context layers — route through this adapter.
shellThe ShellAdapter (defaults to createLocalShellAdapter()). Tools that need a real shell go through this.
subprocessThe SubprocessAdapter used by runCode.subprocess requests and detached spawns. Defaults to an in-memory adapter.
rootCwdStateLong-lived shared cwd state, seeded into every root context the harness creates so successive runs and the TUI observe each other's cds.
callModel(request)Issue a single LLM call, bypassing the session/queue runner. Used by plugins that need one-shot generation.
decide(request)Answer a DecideRequest. Decision models answer natively; any other model answers through a structured-output call that estimates the probabilities. Used by decide steps and aiCondition. See Model Paths.
execute(input, options?)Enqueue a message on the session identified by options.threadId. Resolves once the message is accepted into the queue — not when the model responds.
getAgentResponse(scope?)Resolves with the accumulated response once the session drains.
getItemStream(scope?)Yields cumulative Item snapshots across every turn in the session.
getTextStream(scope?)Yields text deltas across every turn.
getReasoningStream(scope?)Yields reasoning-token deltas.
getFullStream(scope?)Yields all raw SDK + framework events.
abort(scope?)Cancel the in-flight turn. Queued messages are preserved. With drainMs, in-flight model calls get that long to finish first (graceful stop), and it resolves with the DrainReport.
getStatus(scope?)Session status snapshot (HarnessStatus). Pass { executionId } for a run() execution's ExecutionStatus.
getQueueSize(scope?)Messages currently queued on the session.
seedSessionHistory(threadId, items)Pre-populate a session's accumulated history with prior items. Used by resume flows.
setRootCwd(nextCwd)Update the harness root cwd (e.g., from a TUI ! cd).
run(step, input, ctx)Run a step with the given input and context.
detachedSpawn(step, input, parentCtx, overrides?)Launch a step concurrently, returning a DetachedHandle. Optional overrides.threadId / overrides.resourceId / overrides.cwdInit decouple the child's session-scoped state from the parent's.
createContext(opts?)Create a new execution context. Accepts optional context to override harness-level defaults.
send(channel, value, ctx)Send a value to a channel. Returns a Promise<void> that resolves immediately unless the target queue channel is at capacity, in which case the send parks until a consumer frees a slot (back-pressure; default 30s timeout → channel_timeout, abort → cancelled).
recv(channel, ctx, opts?)Receive from a channel (blocking). Rejects with kind 'cancelled' when the context is aborted while waiting.
tryRecv(channel, ctx)Non-blocking receive.
getChannelHandle(channel, executionId)Get external channel handle.
getChannelStream(channel, executionId)Subscribe to an external channel from outside the execution; ends when the root execution completes.
initLayers(layers, ctx, storage)Initialize context layers.
recallLayers(layers, input, ctx)Run recall on all layers.
recallLayersAtomic(layers, input, ctx, budgets, opts?)Run recall on the atomic-mode layers only, blocking until each completes within its budget. Pass { peek: true } to render without keeping returned state (see RecallOptions).
recallLayersEventual(layers, input, ctx, budgets, opts?)Serve recall for eventual-mode layers from cache without blocking; the cache refreshes after store(). A peek renders a cold or stale layer without updating the cache.
projectHistory(layers, items, ctx)Run each layer's projectHistory hook in slot order to project (cap, transform) the history portion of the context window. Read-side only.
runAppendPipeline(layers, items, ctx)Run input items through each layer's onItemAppend hook in slot order, returning the transformed items plus any re-render requests.
executeRerender(requests, layers, ctx, budgets, query?)Re-run recall for the layers selected by the given re-render requests and return their fresh outputs.
previewRequestItems(scope?)Return the Item[] that would be sent on the next turn — accumulated history plus harness-level layer recall outputs. Inits layers on a throwaway context (hydrating scoped state from storage) and drops its state entries afterwards; no session is allocated for unknown thread ids.
storeLayers(layers, response, ctx)Run store on all layers.
disposeLayers(layers, ctx)Dispose all layers.
getLayerState(executionId, layerId)Read a layer's state for a given execution.
setLayerState(executionId, layerId, state)Overwrite a layer's state for a given execution.
beforeToolCall(layers, toolName, toolArgs, ctx)Invoke each layer's beforeToolCall steering hook and aggregate the decision.
afterModelCall(layers, response, ctx)Invoke each layer's afterModelCall steering hook.
onSteer?(layers, params, ctx)Run a steer through each layer's onSteer hook in slot order. Returns a SteerVerdict ({ action, items }). A harness without it applies each steer at its delivery mode's default point and appends it unchanged.
notifyDecision(layers, record, ctx)Run every layer's onDecision hook in slot order, one at a time. Called by decide steps after they validate their answers.
checkpoint(ctx)Persist current execution state.
restore(executionId)Restore a previously checkpointed context.
cancel(ctx, options?)Cancel an execution: abort ctx and every live descendant context, then run layer teardown (onComplete with outcome: 'aborted', then dispose) bottom-up. No-op if already cancelled. options is a reason string or a CancelOptions; with drainMs it is a graceful stop. Resolves with the DrainReport when a drain ran, undefined otherwise.
createSpan(name, parent)Create an observability span.

DeliveryMode

type DeliveryMode = 'next-turn' | 'between-rounds' | 'next-item' | 'interrupt';
ModeBehaviour
next-turn (default)Queue and run after the current turn completes.
between-roundsSteer: add the message before the next model call within the active turn.
next-itemSteer: apply once the model finishes its current output item, stopping the stream there.
interruptAbort the in-flight turn, keep the items it finished, place at head of queue, restart.

ExecuteOptions

interface ExecuteOptions {
  threadId?: string;
  resourceId?: string;
  state?: unknown;
  contextLayers?: ContextLayer[];
  deliveryMode?: DeliveryMode;
  messageId?: string;
}

interface SessionScope {
  threadId?: string;
}

HarnessStatus

type HarnessStatus =
  | { readonly kind: 'idle' }
  | { readonly kind: 'generating'; readonly startedAt: number; readonly turnId: string }
  | { readonly kind: 'draining'; readonly turnId: string; readonly deadline: number }
  | { readonly kind: 'aborting'; readonly turnId: string };

draining is a graceful stop in progress; deadline is when it cuts off whatever is still in flight (epoch ms).

ExecutionStatus

interface ExecutionScope {
  executionId: string; // a root context's id
}

type ExecutionStatus =
  | { readonly kind: 'idle' }
  | { readonly kind: 'generating'; readonly startedAt: number }
  | { readonly kind: 'draining'; readonly deadline: number }
  | { readonly kind: 'aborting' };

What getStatus({ executionId }) returns for a run() execution: generating while a run is in flight on the root context, draining during a graceful stop, aborting from the cancel until the run returns, and idle otherwise.

CancelOptions

interface CancelOptions {
  readonly reason?: string;
  /** Let in-flight model calls finish for up to this many ms before cancelling. */
  readonly drainMs?: number;
}

DrainReport

interface DrainReport {
  readonly executionId: string;
  readonly reason: 'settled' | 'deadline' | 'stopped';
  readonly durationMs: number;
  readonly finished: ReadonlyArray<DrainCall>;
  readonly cutOff: ReadonlyArray<DrainCall>;
}

interface DrainCall {
  readonly kind: 'model_round' | 'decide';
  readonly model: string;
  readonly executionId: string; // the context that made the call
  readonly round?: number;
  readonly dispatchId?: string;
  readonly generationId?: string;
  readonly cost?: number;
  readonly usage?: RoundUsage;
}

What cancel and abort resolve with after a graceful stop, and the data of {name}:drain_finished. A finished call the provider billed carries cost (USD) and usage exactly as charged to its context, a round rejected and asked again included; a cutOff call never does.

HarnessResponse

interface HarnessResponse {
  readonly items: ReadonlyArray<Item>;
  readonly usage: { inputTokens: number; outputTokens: number; cachedTokens?: number };
  readonly cost?: number;       // USD the turn's work cost, replayed work included
  readonly billedCost?: number; // USD the turn actually paid — 0 for replayed work
  readonly heldCost?: number;   // USD held for cut-off paid calls with no settled bill yet
  readonly text: string;
  readonly lastLayerUsage?: LastLayerUsage;
}

StreamEvent

type StreamEvent = SdkStreamEvent | FrameworkStreamEvent;

interface SdkStreamEvent {
  readonly source: 'sdk';
  readonly type: string;
  readonly data: Record<string, unknown>;
  readonly outputIndex?: number;
  readonly contentIndex?: number;
}

interface FrameworkStreamEvent {
  readonly source: 'framework';
  readonly type: `${string}:${string}`;
  readonly data: Record<string, unknown>;
}

StreamingItem

type StreamingItem = Item & { readonly isComplete: boolean };

AgentConfig

Declarative configuration for an agent harness. Generic over TParams, an arbitrary key-value record accessible via ctx.harness.config.params. Filesystem and shell adapters are passed to the AgentHarness constructor via environment and exposed as harness.fs / harness.shell; they are not part of AgentConfig itself.

declare interface AgentConfigShape<
  TParams extends Record<string, unknown> = Record<string, unknown>,
> {
  name: string;
  storage?: StorageAdapter;
  hooks?: AgentHooks;
  params: TParams;
  itemSchemas?: { schemas?: unknown; strict?: boolean };
  projection?: ProjectionPolicy;
  forceAtomicRecall?: boolean;
  contextCache?: unknown;
  decisionModel?: string;
  emitItemsOnReplay?: boolean;
  approveToolCall?: ToolApprover;
}
FieldTypeRequiredDescription
namestringyesAgent name.
storageStorageAdapternoStorage backend for context persistence. Defaults to an in-memory adapter.
hooksAgentHooksnoBefore/after step hooks.
paramsTParamsyesArbitrary key-value parameters available to every step and tool via ctx.harness.config.params.
itemSchemasItemSchemaConfignoItem schema extensions and validation strictness: { schemas?: ItemSchemaExtensions, strict?: boolean }. schemas is global by design; tool-contributed toolResults schemas are owner-scoped to each tool's own result items. Extension schemas are gates, not normalizers — the original item is returned on match. Mismatches raise NoeticError kind item_schema_mismatch. strict (default true) controls whether unknown extension item types must match a registered schema.
projectionProjectionPolicynoDefault projection policy for all callModel steps. Individual steps override it via step.projection.
forceAtomicRecallbooleannoRecall every layer atomically regardless of its recallMode, disabling cached/eventual recall.
contextCacheContextCacheConfignoTuning for prompt-cache anchoring. On by default; pass { enabled: false } to render every layer before history as it did before bands existed.
decisionModelstringnoDefault model for decide steps that set no model of their own, e.g. 'typesafe/jev-1.14'.
emitItemsOnReplaybooleannoDefaults to false. When true, a resumed run streams again, as item_appended, the items its replayed steps emitted through emitItems.
approveToolCallToolApproverno(params: { toolName, toolArgs, callId?, ctx }) => SteeringDecision | Promise<SteeringDecision>. Asked before every call to a tool that declares needsApproval, after the beforeToolCall layers allow it; allow runs the call, anything else refuses it. Without one, those calls run unapproved and the harness warns once per tool name. See Human-in-the-Loop Approval.

DecideRequest

What harness.decide() takes. It uses Noetic's vocabulary; the adapter translates it to each provider's wire format.

interface DecideRequest {
  model: string;
  instructions?: string;
  state: ReadonlyArray<DecideStatePart>;
  questions: DecideQuestions;
  samples?: number;
  signal?: AbortSignal;
}

type DecideStatePart =
  | { type: 'text'; text: string; name?: string }
  | { type: 'json'; name: string; value: unknown }
  | { type: 'image'; imageUrl: string };
FieldTypeDescription
modelstringDecision or chat model id
instructionsstringShared framing for every question (optional)
stateReadonlyArray<DecideStatePart>What the decision is made against: text, named JSON values, and images
questionsDecideQuestionsQuestions keyed by id. See Step Types
samplesnumberRuns averaged on the estimated path. Ignored on the native path. Default 1 (optional)
signalAbortSignalCancels the call (optional)

DecideResponse

interface DecideResponse {
  answers: Readonly<Record<string, DecideAnswer>>;
  source: 'native' | 'estimated';
  modelId: string;
  usage: { inputTokens: number; outputTokens: number };
  cost?: number;
}
FieldTypeDescription
answersReadonly<Record<string, DecideAnswer>>One answer or Refusal per question id
source'native' | 'estimated''native': a decision model's own distribution. 'estimated': a chat model's self-reported probabilities, which are not calibrated
modelIdstringModel that answered, as the provider reports it
usage{ inputTokens, outputTokens }Token usage, summed across samples
costnumberCost in USD (optional)

harness.decide() returns the response as the provider gave it. A decide step then checks it (every question answered or refused, every choice and level known, every probability in [0, 1]) and throws model_parse_error on a mismatch. On the native path, only an explicit provider refusal becomes a Refusal; a missing or mistyped answer is left out, so the step's check fails with model_parse_error. Every answer's probabilities has an entry for each outcome (0 when the provider sent none). On the estimated path, samples that fail or do not parse are dropped and the rest are averaged; when none parse, harness.decide() throws DecideOutputError, which carries the samples' usage and cost. A decide step counts that spend and turns the error into model_parse_error; aiCondition turns it into model_parse_error with step id 'aiCondition'.

AgentHooks

interface AgentHooks {
  beforeStep?: (step: Step, ctx: Context) => Promise<void>;
  afterStep?: (step: Step, result: unknown, ctx: Context) => Promise<void>;
}
HookParametersDescription
beforeStep(step, ctx)Called before each step executes
afterStep(step, result, ctx)Called after each step completes

DetachedHandle

A handle returned by harness.detachedSpawn() to track a concurrently running child step.

interface DetachedHandle<O> {
  readonly id: string;
  readonly status: DetachedStatus;
  readonly result: O | undefined;
  readonly error: string | undefined;
  await(timeout?: number): Promise<O>;
}
PropertyTypeDescription
idstringUnique handle identifier (child context ID)
statusDetachedStatusCurrent execution status
resultO | undefinedChild output (set when completed)
errorstring | undefinedError message (set when failed)
await(timeout?)Promise<O>Wait for completion, optionally with timeout in ms

DetachedStatus

const DetachedStatus = {
  Running: 'running',
  Completed: 'completed',
  Failed: 'failed',
} as const;
type DetachedStatus = (typeof DetachedStatus)[keyof typeof DetachedStatus];
ValueDescription
'running'Child step is still executing
'completed'Child step finished successfully
'failed'Child step threw an error

RecallLayerOutput

The output from a single context layer's recall hook.

interface RecallLayerOutput {
  layerId: string;
  items: Item[];
  tokenCount: number;
  mutatedState?: boolean;
}
FieldTypeDescription
layerIdstringWhich layer produced this output
itemsItem[]Items to inject into the prompt
tokenCountnumberToken cost of the items
mutatedStatebooleanSet when the layer's recall returned new state. Such a layer is never pinned for the prompt cache — serving an older render would drop what the call committed

LLMResponse

The response from an LLM call.

interface RoundUsage {
  inputTokens: number;
  outputTokens: number;
  cachedTokens?: number;
  cacheWriteTokens?: number;
}

interface LLMResponse {
  items: Item[];
  usage: RoundUsage;
  rounds?: ReadonlyArray<RoundUsage>;
  cost?: number;
  billedCost?: number;
  recall?: boolean;
}

usage totals every round of the call, including a round the harness rejected and asked again (a provider error status in a 2xx body, or no output): the provider billed it. rounds breaks it down oldest first, adding a rejected round's usage to the round after it: rounds after the first replay the same assembled view plus appended tool traffic, so they hit the prompt cache regardless of whether the first round did — only rounds[0] answers "was my prefix cached?". cachedTokens and cacheWriteTokens are undefined, not 0, when the provider reports no cache figures at all.

FieldTypeDescription
itemsItem[]Response items (messages, function calls, etc.)
usage.inputTokensnumberInput tokens consumed
usage.outputTokensnumberOutput tokens generated
usage.cachedTokensnumberCached tokens used (optional)
costnumberUSD the work behind the response cost, including rounds replayed from a recorded run (optional)
billedCostnumberUSD this call actually paid: cost less what replayed rounds supplied, so 0 when every round replayed. Absent means the whole cost was paid now (optional)
recallbooleanThe call ended early after applying a steer whose onSteer layers asked for recall. The callModel step runs recall again and continues the call (optional)

On this page