inParallel
Run multiple steps in parallel with race, all, or settle semantics.
Quick Example
import { callModel, inParallel } from '@noetic-tools/core';
const fastest = inParallel({
id: 'fastest-response',
mode: 'race',
paths: (input: string) => [
callModel({
id: 'model-a',
model: 'openai/gpt-4o',
}),
callModel({
id: 'model-b',
model: 'openai/gpt-4o-mini',
}),
],
});What It Does
inParallel runs multiple steps concurrently and combines the results. There are three modes that determine how results are collected.
Every path receives the same input. This differs from an array of steps elsewhere in Noetic, such as a loop's steps, which runs sequentially and passes each step's output to the next (see Running Steps in Order). inParallel is the only operator that runs steps concurrently.
Modes
race
Returns the output of the first step to complete. All other paths are discarded.
import { inParallel, runCode } from '@noetic-tools/core';
const raceExample = inParallel({
id: 'race-providers',
mode: 'race',
paths: (query: string) => [
runCode({
id: 'provider-a',
execute: async (q) => fetchProviderA(q),
}),
runCode({
id: 'provider-b',
execute: async (q) => fetchProviderB(q),
}),
],
});No merge function is needed -- the winner's output is used directly.
all
Waits for every path to complete, then calls merge with an array of all results.
import { inParallel, runCode } from '@noetic-tools/core';
const allExample = inParallel({
id: 'gather-research',
mode: 'all',
paths: (topic: string) => [
runCode({
id: 'academic',
execute: async (t) => searchAcademic(t),
}),
runCode({
id: 'news',
execute: async (t) => searchNews(t),
}),
runCode({
id: 'social',
execute: async (t) => searchSocial(t),
}),
],
merge: (results) => results.flat(),
});If any path throws, the entire inParallel throws fork_partial. The first genuine failure also cancels the remaining paths: siblings still queued behind the concurrency limit are skipped, and in-flight siblings are aborted cooperatively (they stop at their next step boundary or blocked channel operation) and then awaited. Cancelled siblings appear in fork_partial.failed with error.kind === 'cancelled'; paths that completed before the failure stay in succeeded. If the parent context itself is aborted mid-run, the operator throws cancelled instead of fork_partial.
settle
Like all, but collects both successes and failures. Each result is a SettleResult with a status field.
import type { SettleResult } from '@noetic-tools/core';
import { inParallel, runCode } from '@noetic-tools/core';
const settleExample = inParallel({
id: 'resilient-search',
mode: 'settle',
paths: (query: string) => [
runCode({
id: 'source-a',
execute: async (q) => fetchSourceA(q),
}),
runCode({
id: 'source-b',
execute: async (q) => fetchSourceB(q),
}),
],
merge: (results: SettleResult<string>[]) => {
const successes = results
.filter((r) => r.status === 'fulfilled')
.map((r) => r.value);
return successes.join('\n');
},
});SettleResult Type
| Property | Type | Description |
|---|---|---|
stepId | string | The id of the step that produced this result |
status | 'fulfilled' | 'rejected' | Whether the step succeeded or failed |
value | O | undefined | The output value (present when fulfilled) |
error | NoeticError | undefined | The error (present when rejected) |
API Reference
inParallel() takes an InParallelOpts<TContext, I, O>: a union with one shape per mode. merge is required for all and settle and absent for race. Import the type to build options apart from the call:
import type { InParallelOpts, MergeFn } from '@noetic-tools/core';Common Options (all modes)
| Property | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique step identifier |
mode | 'race' | 'all' | 'settle' | Yes | Execution mode |
paths | (input: I, ctx: Context<TContext>) => Step<TContext, I, O>[] | Yes | Function returning the steps to run |
concurrency | number | No | Max parallel executions |
Mode-Specific Options
| Property | Modes | Type | Description |
|---|---|---|---|
merge | all | MergeFn<TContext, O> — (results: O[], ctx: Context<TContext>) => O | Combine all outputs into one |
merge | settle | MergeFn<TContext, O, SettleResult<O>> — (results: SettleResult<O>[], ctx: Context<TContext>) => O | Combine settled results into one |
Concurrency Control
Limit how many paths run at once with concurrency.
import { inParallel } from '@noetic-tools/core';
const throttled = inParallel({
id: 'throttled-parallel',
mode: 'all',
paths: (input: string) => generateManySteps(input),
merge: (results) => results,
concurrency: 3, // at most 3 paths execute simultaneously
});Context Layers
Each path is a child execution. It inherits the parent's context layers and tool pool, so callModel steps inside a path keep their context projection and layer tools, and a nested spawn has layers to inherit.
Layer state is per path:
onSpawnseeds each path's state from the parent's before the path runs. Items returned byonSpawnare not appended -- a parallel path already has the parent's full item log.onReturnmerges a successful path's contribution back into the parent. A failed path is not merged, and its state is discarded.- Merges run one at a time even though paths run concurrently, so no path's contribution is lost to a concurrent read-modify-write.
- While a path runs, a layer's
onParentUpdatehears every write of the parent's state for that layer -- including a sibling'sonReturnmerge. See Parent updates to live children.
For layers that collect artifacts from many workers, choose a merge strategy that namespaces per child -- see taskState({ mergeData: 'namespace' }).
Token and Cost Roll-up
Every path's tokens and cost are charged to the parent context as they are spent, in every mode. A path that fails, is cancelled when a sibling fails, or loses a race still bills the parent; a race loser whose model call finishes after the race resolved charges the parent when that call lands. A loop wrapped around an inParallel therefore stops on until.maxCost at the first check after the paths' combined spend crosses the cap. See Usage roll-up.
Tracing
Each path runs under its own inParallel.path span, a child of the enclosing span, stamped with the path id (noetic.node.id), the inParallel id (noetic.parallel.id), and the path index (noetic.parallel.index). A path's model calls, tool calls, and loop verdicts land on its span. A failed path gets error: true and noetic.error.kind; a cancelled or never-started path gets noetic.error.kind: 'cancelled' only. See Observability.
Related
- conditional -- conditional routing for single-path decisions.
- Spawn -- isolated child contexts for deeper isolation.
- Steps -- the step types you can use as parallel paths.