NOETIC
Framework
API Reference

Step Types

Type definitions for all step variants in Noetic's discriminated union.

Step Union

The Step type is a discriminated union over the kind field:

import type {
  Step,
  StepConditional,
  StepDecide,
  StepInParallel,
  StepCallModel,
  StepLoop,
  StepWithContext,
  StepRunCode,
  StepSpawn,
  StepInvokeTool,
} from '@noetic-tools/core';

type _AssertShape<TContext, I, O> =
  | StepRunCode<TContext, I, O>
  | StepCallModel<TContext, I, O>
  | StepInvokeTool<TContext, I, O>
  | StepDecide<TContext, I, O>
  | StepConditional<TContext, I, O>
  | StepInParallel<TContext, I, O>
  | StepSpawn<TContext, I, O>
  | StepWithContext<TContext, I, O>
  | StepLoop<TContext, I, O>;

type _AssertSubset<TContext, I, O> = _AssertShape<TContext, I, O> extends Step<TContext, I, O>
  ? true
  : false;

The exported Step union additionally includes StepSchedule for fixed-interval scheduling and StepSequence, what an array of steps becomes.

StepReplay

Every step interface extends StepReplay<TContext, I>, so these fields apply to every row below. See Replay Identity.

FieldTypeRequiredDescription
durablebooleannoDefaults to true. false keeps the step out of the step ledger: never replayed or recorded on resume.
replayKey(input: I, ctx: Context<TContext>) => stringnoContent key the ledger uses instead of the execution path.

StepEmitItems

Every step interface also extends StepEmitItems<TContext, I, O, M>. See Emitting Items Early and Custom Items.

FieldTypeRequiredDescription
emitItemsStepEmitItemsOption<TContext, I, O, M>noWhat to add to the conversation as the step runs. Items are appended to the item log and streamed as item_appended; a custom item's events stream as custom_item_event. A step that throws emits nothing. A replay streams them again only under the harness's emitItemsOnReplay.
customItemsM extends CustomItemMapnoCustom items only this step may emit, by name.
TypeDefinition
StepEmitItemsOption<TContext, I, O, M>boolean | StepEmitItemsFn<TContext, O> | StepEmitItemsConfig<TContext, I, O, M>. true emits the output as a final item.
StepEmitItemsFn<TContext, O>(output: O, ctx: Context<TContext>) => StepEmitItemsResult | Promise<StepEmitItemsResult>
StepEmitItemsResultstring | Item | ReadonlyArray<Item> | undefined. A string becomes an assistant message; items are appended as given and must pass the context's item schemas; undefined or '' emits nothing.
StepEmitItemsConfig<TContext, I, O, M>{ item: N; onStart?: EmitHook; onDone?: EmitHook } for each item name N in M, with hooks typed by that item, or { onStart?, onDone? } without an item, giving text or standard items.
EmitHook<TContext, V, E, F>(value: V, ctx) => F | undefined | Promise<…> | Generator<E, F | undefined> | AsyncGenerator<E, F | undefined>: yields are events E, the return value the final fields F.

StepRunCode

StepRunCode<TContext, I, O>

FieldTypeRequiredDescription
kind'runCode'yesDiscriminant
idstringyesStep identifier
execute(input: I, ctx: Context<TContext>) => Promise<O>yesAsync function to execute
retryRetryPolicynoRetry configuration

StepCallModel

StepCallModel<TContext, I, O>

FieldTypeRequiredDescription
kind'callModel'yesDiscriminant
idstringyesStep identifier
modelstringyesModel identifier
instructionsstringnoSystem prompt / instructions for the model
toolsTool[]noAllowed tool subset (undefined = all, [] = none)
toolChoiceToolChoiceno'auto' (default), 'required', 'none', or { tool: name }; see Tool Choice
outputStandardSchemaV1<unknown, O> | OutputCodec<O>noStructured output schema (any Standard Schema v1; Zod is the default) or a streaming codec
outputJsonSchemaRecord<string, unknown>noExplicit JSON Schema override/fallback for non-Zod output. Conversion order is Zod → StandardJSONSchemaV1 → this field; validation-only schemas require it
paramsModelParamsnoSampling parameters
emitboolean | ((eventType, data) => boolean)noControls framework event emission for this step. Defaults to true. Pass false to suppress all framework events, or a filter function to allow specific events through.

StepInvokeTool

StepInvokeTool<TContext, I, O>

FieldTypeRequiredDescription
kind'invokeTool'yesDiscriminant
idstringyesStep identifier
toolTool<StandardSchemaV1<unknown, I>, StandardSchemaV1<unknown, O>>yesTool definition
argsPartial<I>noPreset arguments

StepDecide

StepDecide<TContext, I, O>

A step that asks a model typed questions and returns typed answers with probabilities. It reads the conversation but never appends to it, and runs no steering hooks. Built by decide().

FieldTypeRequiredDescription
kind'decide'yesDiscriminant
idstringyesStep identifier
modelLazy<string, TContext>noDecision model id. Falls back to harness.config.decisionModel
instructionsLazy<string | undefined, TContext>noShared framing for every question
questionsDecideQuestionsyesQuestion map keyed by question id
view'full' | 'layers' | 'none'yesHow much of the assembled view the model sees. decide() defaults it to 'full'
context(ctx: Context<TContext>) => Record<string, unknown> | Promise<Record<string, unknown>>noTyped state read from context layers, sent as one JSON entry named context
projectionProjectionPolicynoBudget for the decision's view. Defaults to DECISION_PROJECTION
samplesnumberyesRuns averaged on the estimated path. decide() defaults it to 1
emitboolean | ((eventType, data) => boolean)noControls framework event emission for this step
_answersOnoPhantom field carrying the answers type for inference. Never set at runtime

DecideStep

type DecideStep<TContext, I, Q extends DecideQuestions> =
  StepDecide<TContext, I, DecideAnswers<Q>> & { readonly questions: Q };

What decide() returns: a StepDecide that keeps its literal question map, so routeSwitch can infer the questions, choices, and levels it routes by. DecideAnswers<Q> maps each question id to its answer type or Refusal; AnsweredAll<Q> is the same with Refusal removed.

decide() infers Q from questions; pass it no type arguments. To type ctx.context in the context selector, annotate its parameter ((ctx: Context<MyContext>) => …).

Questions and answers

type DecideQuestions = Readonly<Record<string, DecideQuestion>>;
type DecideQuestion = BooleanQuestion | ChoiceQuestion | ScoreQuestion;

interface BooleanQuestion {
  type: 'boolean';
  instructions?: string;
  criteria?: { true?: string; false?: string };
  threshold?: number;
}

interface ChoiceQuestion<K extends string = string> {
  type: 'choice';
  instructions?: string;
  choices: readonly K[] | Readonly<Record<K, string>>;
}

interface ScoreQuestion<L extends string = string> {
  type: 'score';
  instructions?: string;
  levels: readonly L[] | ReadonlyArray<{ label: L; description?: string }>;
}

type DecideAnswer = BooleanAnswer | ChoiceAnswer | ScoreAnswer | Refusal;

interface BooleanAnswer {
  type: 'boolean';
  value: boolean;
  probability: number;
}

interface ChoiceAnswer<K extends string = string> {
  type: 'choice';
  value: K;
  confidence: number;
  probabilities: Readonly<Record<K, number>>;
}

interface ScoreAnswer<L extends string = string> {
  type: 'score';
  value: number;
  level: L;
  confidence: number;
  probabilities: Readonly<Record<L, number>>;
}

interface Refusal {
  type: 'refusal';
}

See decide for what each field means.

StepConditional

StepConditional<TContext, I, O>

FieldTypeRequiredDescription
kind'conditional'yesDiscriminant
idstringyesStep identifier
route(input: I, ctx: Context<TContext>, run: ExecuteStepFn) => Step<TContext, I, O> | null | Promise<Step<TContext, I, O> | null>yesRouting function (null skips). run executes a step through the interpreter, for routes that must run a step (such as a decision) to choose. See ExecuteStepFn

StepInParallel

inParallel has three mode variants, each a separate interface.

StepInParallelRace

StepInParallelRace<TContext, I, O>

FieldTypeRequiredDescription
kind'inParallel'yesDiscriminant
idstringyesStep identifier
mode'race'yesFirst completion wins
paths(input: I, ctx: Context<TContext>) => Step<TContext, I, O>[]yesPath factory
concurrencynumbernoMax parallel paths

StepInParallelAll

StepInParallelAll<TContext, I, O>

FieldTypeRequiredDescription
kind'inParallel'yesDiscriminant
idstringyesStep identifier
mode'all'yesWait for all paths
paths(input: I, ctx: Context<TContext>) => Step<TContext, I, O>[]yesPath factory
mergeMergeFn<TContext, O>yesMerge function
concurrencynumbernoMax parallel paths

StepInParallelSettle

StepInParallelSettle<TContext, I, O>

FieldTypeRequiredDescription
kind'inParallel'yesDiscriminant
idstringyesStep identifier
mode'settle'yesWait for all, include errors
paths(input: I, ctx: Context<TContext>) => Step<TContext, I, O>[]yesPath factory
mergeMergeFn<TContext, O, SettleResult<O>>yesMerge function
concurrencynumbernoMax parallel paths

StepWithContext

StepWithContext<TContext, I, O>

FieldTypeRequiredDescription
kind'withContext'yesDiscriminant
idstringyesStep identifier
childStep<TContext, I, O>yesChild step to execute with the provided layers
contextContextConfig | ContextLayer[]yesContext layers available to all descendant steps

StepSpawn

StepSpawn<TContext, I, O>

FieldTypeRequiredDescription
kind'spawn'yesDiscriminant
idstringyesStep identifier
childStep<TContext, I, O>yesChild step to execute
contextContextConfig | ContextLayer[]noSpawn-local context layers
timeoutnumbernoTimeout in milliseconds

StepLoop

StepLoop<TContext, I, O>

FieldTypeRequiredDescription
kind'loop'yesDiscriminant
idstringyesStep identifier
stepsReadonlyArray<Step<TContext, I, O>>yesLoop body steps
untilUntil | StepDecide<TContext>yesTermination predicate, or a decide step asking one boolean question (yes stops)
maxIterationsnumbernoHard cap on iterations, retries included (default 1000). A positive integer or Infinity
maxHistorySizenumbernoMax history items to retain
inboxChannel<string>noChannel for external messages that prevent loop from stopping
parkTimeoutnumbernoMilliseconds to wait on inbox before stopping (0 = non-blocking)
prepareNext(output: O, verdict: Verdict, ctx: Context<TContext>) => InoTransform output to next input
onError(error: NoeticError, ctx: Context<TContext>) => 'retry' | 'skip' | 'abort'noError handler

StepSequence

StepSequence<TContext, I, O>

What an array of steps becomes. An array of steps runs in sequence, each step's output the next one's input, wherever these take a step: harness.run and ctx.harness.run, harness.detachedSpawn and ctx.harness.detachedSpawn, agentGraph, spawn and withContext children, schedule bodies, inParallel paths, a conditional route's pick, and when/otherwise clauses. There is no step.sequence; write the array.

A loop's steps, a wait's around and a verify's act take a list of single steps and run it in sequence themselves, without a sequence step. decide/routeSwitch cases (and uncertain/default) and a loop's until decide step take a single step only.

FieldTypeRequiredDescription
kind'sequence'yesDiscriminant
idstringyesThe children's ids joined with >
stepsReadonlyArray<Step<TContext, I, O>>yesThe steps, in order; at least two

An empty array throws EMPTY_STEP_ARRAY; a one-element array is its element. An element that is not a step (a nested array, null, an object without a step kind and string id) throws INVALID_STEP_IN_ARRAY: nested arrays are rejected, not flattened. The sequence keeps a frozen copy of the array, so changing the array after building changes nothing.

StepArray, StepArrayInput and StepArrayOutput

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

const trim = runCode({ id: 'trim', execute: async (input: string) => input.trim() });
const count = runCode({ id: 'count', execute: async (input: string) => input.length });

const harness = new AgentHarness({ name: 'arrays', params: {} });
const length: number = await harness.run([trim, count], '  hi  ', harness.createContext());

Each element is a StepLike: an object with a step kind and that kind's required fields, so a bare { kind, id } lookalike is not a step. Every step carries a type-only marker, '~types'?: StepTypes<I, O> (never set at runtime), from which an element's input and output are read. run and detachedSpawn infer the array as a tuple and check every link (ValidStepChain<T>): each step must take the previous step's output, unless that output is typed unknown (as a call step's is). The input is the first step's (StepsInput<T>, StepArrayInput<T>) and the output the last step's (StepsOutput<T>, StepArrayOutput<T>). They also accept a Step<C, T, T>[] variable and a Steps<C, I, O> value passed through generic code.

Steps<TContext, I, O> is what the builders and agentGraph accept: a step, a StepChain<I, O> (a non-empty tuple whose first step takes I and last step outputs O), or HomogeneousSteps<TContext, I, O> (an array of Step<TContext, T, T> when I and O are both T). A builder checks an array's ends against its I and O; only run and detachedSpawn check the links between.

StepSchedule

StepSchedule<TContext, I, O>

A step that runs a body step on a fixed-interval schedule, optionally woken sooner by a wake channel. Runs forever until the executing context is cancelled. The operator output is void — schedule does not accumulate iteration outputs.

FieldTypeRequiredDescription
kind'schedule'yesDiscriminant
idstringyesStep identifier
stepStep<TContext, I, O>yesBody step executed on each iteration
intervalnumberyesPark duration between iterations in milliseconds. Must be >= 0.
inboxChannel<unknown>noChannel that wakes the parking interval when any value arrives
onError'continue' | 'fail'noBehavior when step throws. Defaults to 'continue' (record the error and keep parking).
jitternumbernoRandom jitter applied to the park duration in milliseconds. Must be >= 0. Defaults to 0.

Supporting Types

SettleResult

FieldTypeDescription
stepIdstringStep that produced this result
status'fulfilled' | 'rejected'Outcome
valueOPresent if fulfilled
errorNoeticErrorPresent if rejected

MergeFn

MergeFn<TContext, O, R = O> combines inParallel path results into one output. R is the element type of results: O in all mode, SettleResult<O> in settle mode.

type MergeFn<TContext = ContextData, O = unknown, R = O> = (
  results: R[],
  ctx: Context<TContext>,
) => O;

RetryPolicy

FieldTypeDescription
maxAttemptsnumberMaximum retry attempts
backoff'fixed' | 'linear' | 'exponential'Backoff strategy
initialDelaynumberInitial delay in milliseconds
maxDelaynumberMaximum delay cap (optional)

ModelParams

FieldTypeDescription
temperaturenumberSampling temperature (optional)
topPnumberNucleus sampling (optional)
maxTokensnumberMax response tokens (optional)
stopSequencesstring[]Deprecated and ignored (optional) — the OpenRouter Responses API has no stop parameter, so it is never sent
serviceTierServiceTierOpenRouter service tier (optional); the served tier lands on StepMeta.serviceTier

ToolChoice

ToolChoiceMode | { tool: string }. ToolChoiceMode is an ESM literal enum: Auto ('auto'), Required ('required'), None ('none'). { tool } forces one client tool by name.

Tool

FieldTypeDescription
namestringTool name (used by the LLM for selection).
descriptionstringTool description shown to the LLM.
inputStandardSchemaV1<I>Schema validating tool input arguments (any Standard Schema v1; Zod is the default).
outputStandardSchemaV1<O>Schema validating tool return value.
inputJsonSchemaRecord<string, unknown>Explicit JSON Schema override/fallback for non-Zod input. Conversion order is Zod → StandardJSONSchemaV1 → this field; validation-only schemas require it.
eventStandardSchemaV1Optional schema validating streaming events yielded during execution.
itemSchemasItemSchemaExtensionsOptional item schemas for tool-call/result extensions contributed by this tool. toolResults schemas are owner-scoped: they validate only this tool's own result items and never reject a sibling tool's results. Schemas are pure shape validators (gates, not normalizers) — the original item is returned on match, and Zod transforms/defaults are unsupported.
decorateResultItem(params) => ItemDecorate the harness-created tool-result item before it is appended/emitted. The decorated item must satisfy this tool's own itemSchemas.toolResults (when declared) — including for error outputs such as malformed-arguments results — or the round fails with NoeticError kind item_schema_mismatch.
execute(args: InferSchemaOutput<I>, toolCtx: ToolExecutionContext) => Promise<InferSchemaOutput<O>> | AsyncGenerator<unknown, InferSchemaOutput<O>>Async function (or generator) that performs the tool's work. InferSchemaOutput is z.output for Zod schemas and StandardSchemaV1.InferOutput otherwise.
needsApprovalbooleanOptional. Every call needs approval: after the beforeToolCall layers allow it, the harness asks its approveToolCall approver. A refused call returns an error tool result to the model, or throws steering_denied from invokeTool. With no approver configured the call runs and the harness warns once per tool name.
contextToolContextDeclarationOptional. Declares tool-owned state the runtime materializes into a ContextLayer.

ExecuteStepFn

declare const executeStepShape: <TContext, I, O>(
  step: Step<TContext, I, O>,
  input: I,
  ctx: Context<TContext>,
) => Promise<O>;

StepInput and StepOutput

import { runCode } from '@noetic-tools/core';
import type { StepInput, StepOutput } from '@noetic-tools/core';

const count = runCode({ id: 'count', execute: async (input: string) => input.length });

type CountIn = StepInput<typeof count>;   // string
type CountOut = StepOutput<typeof count>; // number

Pull a step's input or output type out of its type. routeSwitch uses them to type its output as the union of every case's output.

On this page