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.
| Field | Type | Required | Description |
|---|---|---|---|
durable | boolean | no | Defaults to true. false keeps the step out of the step ledger: never replayed or recorded on resume. |
replayKey | (input: I, ctx: Context<TContext>) => string | no | Content 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.
| Field | Type | Required | Description |
|---|---|---|---|
emitItems | StepEmitItemsOption<TContext, I, O, M> | no | What 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. |
customItems | M extends CustomItemMap | no | Custom items only this step may emit, by name. |
| Type | Definition |
|---|---|
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> |
StepEmitItemsResult | string | 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>
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'runCode' | yes | Discriminant |
id | string | yes | Step identifier |
execute | (input: I, ctx: Context<TContext>) => Promise<O> | yes | Async function to execute |
retry | RetryPolicy | no | Retry configuration |
StepCallModel
StepCallModel<TContext, I, O>
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'callModel' | yes | Discriminant |
id | string | yes | Step identifier |
model | string | yes | Model identifier |
instructions | string | no | System prompt / instructions for the model |
tools | Tool[] | no | Allowed tool subset (undefined = all, [] = none) |
toolChoice | ToolChoice | no | 'auto' (default), 'required', 'none', or { tool: name }; see Tool Choice |
output | StandardSchemaV1<unknown, O> | OutputCodec<O> | no | Structured output schema (any Standard Schema v1; Zod is the default) or a streaming codec |
outputJsonSchema | Record<string, unknown> | no | Explicit JSON Schema override/fallback for non-Zod output. Conversion order is Zod → StandardJSONSchemaV1 → this field; validation-only schemas require it |
params | ModelParams | no | Sampling parameters |
emit | boolean | ((eventType, data) => boolean) | no | Controls 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>
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'invokeTool' | yes | Discriminant |
id | string | yes | Step identifier |
tool | Tool<StandardSchemaV1<unknown, I>, StandardSchemaV1<unknown, O>> | yes | Tool definition |
args | Partial<I> | no | Preset 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().
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'decide' | yes | Discriminant |
id | string | yes | Step identifier |
model | Lazy<string, TContext> | no | Decision model id. Falls back to harness.config.decisionModel |
instructions | Lazy<string | undefined, TContext> | no | Shared framing for every question |
questions | DecideQuestions | yes | Question map keyed by question id |
view | 'full' | 'layers' | 'none' | yes | How much of the assembled view the model sees. decide() defaults it to 'full' |
context | (ctx: Context<TContext>) => Record<string, unknown> | Promise<Record<string, unknown>> | no | Typed state read from context layers, sent as one JSON entry named context |
projection | ProjectionPolicy | no | Budget for the decision's view. Defaults to DECISION_PROJECTION |
samples | number | yes | Runs averaged on the estimated path. decide() defaults it to 1 |
emit | boolean | ((eventType, data) => boolean) | no | Controls framework event emission for this step |
_answers | O | no | Phantom 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>
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'conditional' | yes | Discriminant |
id | string | yes | Step identifier |
route | (input: I, ctx: Context<TContext>, run: ExecuteStepFn) => Step<TContext, I, O> | null | Promise<Step<TContext, I, O> | null> | yes | Routing 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>
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'inParallel' | yes | Discriminant |
id | string | yes | Step identifier |
mode | 'race' | yes | First completion wins |
paths | (input: I, ctx: Context<TContext>) => Step<TContext, I, O>[] | yes | Path factory |
concurrency | number | no | Max parallel paths |
StepInParallelAll
StepInParallelAll<TContext, I, O>
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'inParallel' | yes | Discriminant |
id | string | yes | Step identifier |
mode | 'all' | yes | Wait for all paths |
paths | (input: I, ctx: Context<TContext>) => Step<TContext, I, O>[] | yes | Path factory |
merge | MergeFn<TContext, O> | yes | Merge function |
concurrency | number | no | Max parallel paths |
StepInParallelSettle
StepInParallelSettle<TContext, I, O>
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'inParallel' | yes | Discriminant |
id | string | yes | Step identifier |
mode | 'settle' | yes | Wait for all, include errors |
paths | (input: I, ctx: Context<TContext>) => Step<TContext, I, O>[] | yes | Path factory |
merge | MergeFn<TContext, O, SettleResult<O>> | yes | Merge function |
concurrency | number | no | Max parallel paths |
StepWithContext
StepWithContext<TContext, I, O>
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'withContext' | yes | Discriminant |
id | string | yes | Step identifier |
child | Step<TContext, I, O> | yes | Child step to execute with the provided layers |
context | ContextConfig | ContextLayer[] | yes | Context layers available to all descendant steps |
StepSpawn
StepSpawn<TContext, I, O>
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'spawn' | yes | Discriminant |
id | string | yes | Step identifier |
child | Step<TContext, I, O> | yes | Child step to execute |
context | ContextConfig | ContextLayer[] | no | Spawn-local context layers |
timeout | number | no | Timeout in milliseconds |
StepLoop
StepLoop<TContext, I, O>
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'loop' | yes | Discriminant |
id | string | yes | Step identifier |
steps | ReadonlyArray<Step<TContext, I, O>> | yes | Loop body steps |
until | Until | StepDecide<TContext> | yes | Termination predicate, or a decide step asking one boolean question (yes stops) |
maxIterations | number | no | Hard cap on iterations, retries included (default 1000). A positive integer or Infinity |
maxHistorySize | number | no | Max history items to retain |
inbox | Channel<string> | no | Channel for external messages that prevent loop from stopping |
parkTimeout | number | no | Milliseconds to wait on inbox before stopping (0 = non-blocking) |
prepareNext | (output: O, verdict: Verdict, ctx: Context<TContext>) => I | no | Transform output to next input |
onError | (error: NoeticError, ctx: Context<TContext>) => 'retry' | 'skip' | 'abort' | no | Error 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.
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'sequence' | yes | Discriminant |
id | string | yes | The children's ids joined with > |
steps | ReadonlyArray<Step<TContext, I, O>> | yes | The 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.
| Field | Type | Required | Description |
|---|---|---|---|
kind | 'schedule' | yes | Discriminant |
id | string | yes | Step identifier |
step | Step<TContext, I, O> | yes | Body step executed on each iteration |
interval | number | yes | Park duration between iterations in milliseconds. Must be >= 0. |
inbox | Channel<unknown> | no | Channel that wakes the parking interval when any value arrives |
onError | 'continue' | 'fail' | no | Behavior when step throws. Defaults to 'continue' (record the error and keep parking). |
jitter | number | no | Random jitter applied to the park duration in milliseconds. Must be >= 0. Defaults to 0. |
Supporting Types
SettleResult
| Field | Type | Description |
|---|---|---|
stepId | string | Step that produced this result |
status | 'fulfilled' | 'rejected' | Outcome |
value | O | Present if fulfilled |
error | NoeticError | Present 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
| Field | Type | Description |
|---|---|---|
maxAttempts | number | Maximum retry attempts |
backoff | 'fixed' | 'linear' | 'exponential' | Backoff strategy |
initialDelay | number | Initial delay in milliseconds |
maxDelay | number | Maximum delay cap (optional) |
ModelParams
| Field | Type | Description |
|---|---|---|
temperature | number | Sampling temperature (optional) |
topP | number | Nucleus sampling (optional) |
maxTokens | number | Max response tokens (optional) |
stopSequences | string[] | Deprecated and ignored (optional) — the OpenRouter Responses API has no stop parameter, so it is never sent |
serviceTier | ServiceTier | OpenRouter 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
| Field | Type | Description |
|---|---|---|
name | string | Tool name (used by the LLM for selection). |
description | string | Tool description shown to the LLM. |
input | StandardSchemaV1<I> | Schema validating tool input arguments (any Standard Schema v1; Zod is the default). |
output | StandardSchemaV1<O> | Schema validating tool return value. |
inputJsonSchema | Record<string, unknown> | Explicit JSON Schema override/fallback for non-Zod input. Conversion order is Zod → StandardJSONSchemaV1 → this field; validation-only schemas require it. |
event | StandardSchemaV1 | Optional schema validating streaming events yielded during execution. |
itemSchemas | ItemSchemaExtensions | Optional 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) => Item | Decorate 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. |
needsApproval | boolean | Optional. 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. |
context | ToolContextDeclaration | Optional. 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>; // numberPull 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.