TypeScript API: Patterns

Ready-made multi-agent designs: sequential, parallel, loops, routers, debates and more. Generated from the declarations and doc comments of criogenai 0.1.7. See also: TypeScript API overview, the same module in Python.

Design patterns for agentic systems, ready to use.

import { patterns } from "criogenai";
const flow = patterns.quick("researcher > writer", { model: "openai:gpt-5-mini" });
console.log(String(await flow.run("Write about solar panels")));

Steps can be an Agent, a Specialist or its registered name (a string), a plain function text => text (sync or async), or [name, step]. The model for specialists comes from model or the CRIOGEN_MODEL environment variable. Every loop has a cap, so nothing runs forever.

Workflow patterns return a Pattern: run for the answer, compile(storage) for durable threads, approvals and an audit trail, workflow to extend the graph. react, coordinator and hierarchical return an Agent, so they nest as steps of other patterns.

import { patterns } from "criogenai";
patterns.sequential

These are used as patterns.<name>.

Contents#

NameKindSummary
sequentialfunctionRuns the steps in a fixed order; each one's output is the next one's input.
parallelfunctionRuns the branches at the same time on the same input, then combines them: with a synthesizer step (it receives the input and every branch's answer), or as a titled list in branch order.
loopfunctionRepeats the steps (a chain) until until(text, round) is true or maxIterations is reached (default 5, at most 50).
customfunctionCustom logic: any Workflow, with the same run / compile as the ready-made patterns.
CEILINGconstantNo loop runs more often than this, whatever the caller asks for.
DEFAULT_MODEL_ENVconstantThe environment variable that names the model for specialists when a pattern gets no model.
SteptypeOne step of a pattern: an Agent, a Specialist, a registered specialist's name (a string), a function text => text (sync or async), or [name, step] to name the step in the trace.
PatternResultclassWhat a pattern produced.
PatternclassA workflow pattern: run it, compile it for durability, or extend it.
iterativeRefinementfunctionImproves a result step by step: refine gets the current text and returns a better one; stops when score(text) >= threshold or after maxIterations.
reviewAndCritiquefunctionA generator writes, a critic reviews.
swarmfunctionAgents hand the work to each other with no central coordinator.
humanReviewfunctionRuns a step, then pauses for a person to look at its draft.
quickfunctionA pattern from one line of registered specialist names (plug and play):
reactfunctionA single agent in a reason-act loop with a hard cap on model calls (default 8, at most 50).
coordinatorfunctionAn agent that reads each request and hands it to the right specialist, which it calls as a tool.
hierarchicalfunctionA coordinator of coordinators: the root splits the work, each level hands it further down, workers do it.

sequential#

function sequential(steps: Step[], options?: { model?: Model; }): Pattern

Runs the steps in a fixed order; each one's output is the next one's input. quick("researcher > writer") is the one-liner.

Parameters

  • steps (Step[]): At least one step.
  • options ({ model?: Model; }, default {}): model: the model for specialist steps (else the CRIOGEN_MODEL environment variable).

Raises

  • Error: No steps, or a specialist step and no model.
  • TypeError: A step is not an Agent, a Specialist (or its name) or a function.

Example

import { patterns } from "criogenai";

const flow = patterns.sequential([
  ["trim", (text: string) => text.trim()],
  ["shout", async (text: string) => text.toUpperCase()],
]);
const result = await flow.run("  hello  ");
console.log(String(result)); // "HELLO"
console.log(result.explain()); // ended: done, then one line per step

parallel#

function parallel(branches: Step[] | Record<string, Step>, options?: { synthesizer?: Step; model?: Model; }): Pattern

Runs the branches at the same time on the same input, then combines them: with a synthesizer step (it receives the input and every branch's answer), or as a titled list in branch order.

Parameters

  • branches (Step[] | Record<string, Step>): The steps, as a list or as { name: step } (the names title the combined list).
  • options ({ synthesizer?: Step; model?: Model; }, default {}): synthesizer: a step that writes one answer from the request and the branches' answers. model: the model for specialist steps.

Raises

  • Error: No branches.

Example

import { patterns } from "criogenai";

const flow = patterns.parallel({
  words: (text: string) => `${text.split(" ").length} words`,
  letters: (text: string) => `${text.length} characters`,
});
console.log(String(await flow.run("the quick brown fox")));
// ## words
// 4 words
//
// ## letters
// 19 characters

loop#

function loop(steps: Step[], until: (text: string, round: number) => boolean, options?: { maxIterations?: number; model?: Model; }): Pattern

Repeats the steps (a chain) until until(text, round) is true or maxIterations is reached (default 5, at most 50). stopReason says which.

Parameters

  • steps (Step[]): The chain to repeat; each round starts from the last round's output.
  • until ((text: string, round: number) => boolean): Checked after each round with the output and the round number (from 1): true stops (stopReason "condition").
  • options ({ maxIterations?: number; model?: Model; }, default {}): maxIterations (a whole number of at least 1; values above 50 count as 50) and model for specialist steps.

Raises

  • RangeError: maxIterations is not a whole number of at least 1.

Example

import { patterns } from "criogenai";

const flow = patterns.loop([(text: string) => text + "!"], (text) => text.endsWith("!!!"), { maxIterations: 10 });
const result = await flow.run("go");
console.log(String(result), result.stopReason); // "go!!!" "condition"

custom#

function custom(workflow: Workflow, options?: { name?: string; maxSteps?: number; }): Pattern

Custom logic: any Workflow, with the same run / compile as the ready-made patterns. Start state { input }; the answer is state.output.

Parameters

  • workflow (Workflow): A workflow whose nodes read state.input and write state.output (and optionally trace and stop_reason).
  • options ({ name?: string; maxSteps?: number; }, default {}): name (default "custom") and maxSteps, the step limit for run (default 64).

Example

import { END, Workflow, patterns } from "criogenai";

const wf = new Workflow()
  .node("shout", (state) => ({ output: String(state.input).toUpperCase() }))
  .start("shout")
  .edge("shout", END);
const result = await patterns.custom(wf).run("hello");
console.log(String(result)); // "HELLO"

CEILING#

const CEILING: 50

No loop runs more often than this, whatever the caller asks for.

DEFAULT_MODEL_ENV#

const DEFAULT_MODEL_ENV: "CRIOGEN_MODEL"

The environment variable that names the model for specialists when a pattern gets no model.

Step#

type Step = Agent<any> | specialists.Specialist | string | ((text: string) => unknown) | [string, Step];

One step of a pattern: an Agent, a Specialist, a registered specialist's name (a string), a function text => text (sync or async), or [name, step] to name the step in the trace.

PatternResult#

class PatternResult {
  constructor(output: string | null, trace: Entry[], state: State, stopReason: string, suspended: boolean, waitingFor: unknown, raw: WorkflowResult);
}

What a pattern produced. String(result) is the output; result.explain() shows each step.

Parameters

  • output (string | null): The final answer, or null while the run waits for a person. The final answer, or null while suspended.
  • trace (Entry[]): Each step and what it returned ({ step, output }), in order. Each step and what it returned, in order.
  • state (State): The workflow's final state.
  • stopReason (string): Why it ended: done, condition, approved, max_iterations, max_rounds, final, max_handoffs, unknown_handoff, rejected or suspended. Why it ended.
  • suspended (boolean): true when a humanReview run waits for a person: continue with pattern.resume. true when a person must answer first.
  • waitingFor (unknown): What a person is asked, when suspended. What the person is asked, when suspended.
  • raw (WorkflowResult): The workflow result it was made from.

PatternResult.toString#

toString(): string

The output as text ("" when there is none), so String(result) is the answer.

PatternResult.explain#

explain(): string

Each step on its own line: which step, what it returned (shortened).

Pattern#

class Pattern {
  constructor(name: string, workflow: Workflow, budget: number);
}

A workflow pattern: run it, compile it for durability, or extend it.

Returned by sequential, parallel, loop, iterativeRefinement, reviewAndCritique, swarm, humanReview, quick and custom.

Parameters

  • name (string): The pattern's name, e.g. "sequential" or "review_and_critique". The pattern's name, e.g. "sequential".
  • workflow (Workflow): The plain Workflow: add nodes and edges to extend it before the first run. The workflow that implements it.
  • budget (number): The step limit for run when none is given.

Pattern.compile#

compile(storage?: string | Storage): CompiledWorkflow

The checked, durable form: compile(storage).thread(id).run({ input }), with approvals, resume and an audit trail.

The state starts as { input }; the answer is in state.output.

Parameters

  • storage (string | Storage, optional): A Storage or a URL for the threads; in memory without one.

Raises

  • ValidationError: The workflow (after your changes) does not validate.

Pattern.run#

run(text: string, options?: { maxSteps?: number; signal?: any; }): Promise<PatternResult>

Runs once. A humanReview pattern returns suspended: true; continue with resume.

Parameters

  • text (string): The request: the first step's input.
  • options ({ maxSteps?: number; signal?: any; }, default {}): maxSteps: the run's step limit (each pattern sets one from its own caps). signal: aborting it cancels the run.

Returns: The answer (String(result)), the trace and why it ended.

Pattern.resume#

resume(paused: PatternResult, decision: unknown): Promise<PatternResult>

Continues a humanReview run: "approve", { decision: "edit", text } or { decision: "reject" }.

Works on the result of this object's own run. For a pause that must outlive the process, use compile(storage).thread(id) instead.

Parameters

  • paused (PatternResult): The suspended result of run.
  • decision (unknown): "approve" keeps the draft, { decision: "edit", text } replaces it, { decision: "reject" } ends with an empty output and stopReason "rejected".

Raises

  • Error: paused is not a run of this pattern that waits for a person.

iterativeRefinement#

function iterativeRefinement(refine: Step, score: (text: string) => number, options?: { threshold?: number; maxIterations?: number; model?: Model; }): Pattern

Improves a result step by step: refine gets the current text and returns a better one; stops when score(text) >= threshold or after maxIterations.

Parameters

  • refine (Step): The step that rewrites the text.
  • score ((text: string) => number): Rates a text, typically from 0 to 1.
  • options ({ threshold?: number; maxIterations?: number; model?: Model; }, default {}): threshold (default 0.8), maxIterations (default 5, at most 50) and model for a specialist step.

Example

import { patterns } from "criogenai";

const flow = patterns.iterativeRefinement(
  (text: string) => text + " More detail.",
  (text) => Math.min(text.length / 40, 1),
);
console.log((await flow.run("A short note.")).stopReason); // "condition"

reviewAndCritique#

function reviewAndCritique(generator: Step, critic: Step, options?: { maxRounds?: number; approved?: ((reply: string) => boolean); model?: Model; }): Pattern

A generator writes, a critic reviews. The critic approves (its reply starts with APPROVED, or approved(reply) is true) or returns feedback; the generator revises. Ends on approval or after maxRounds (default 3, at most 50) with the last draft. stopReason is "approved" or "max_rounds".

Parameters

  • generator (Step): The step that writes and revises. From the second round it gets the request, its previous draft and the critic's feedback.
  • critic (Step): The step that reviews; it gets the request and the draft.
  • options ({ maxRounds?: number; approved?: ((reply: string) => boolean); model?: Model; }, default {}): maxRounds (default 3, at most 50), approved (decides from the critic's reply; default: it starts with "APPROVED"), model for specialist steps.

Raises

  • RangeError: maxRounds is not a whole number of at least 1.

Example

import { Agent, FakeModel, patterns } from "criogenai";

const writer = new Agent({ model: new FakeModel(["Draft one.", "Draft two, with sources."]) });
const critic = new Agent({ model: new FakeModel(["Add sources.", "APPROVED"]) });
const result = await patterns.reviewAndCritique(writer, critic).run("Write a short note on hand washing.");
console.log(String(result), result.stopReason); // "Draft two, with sources." "approved"

swarm#

function swarm(agents: Record<string, Step>, options?: { start?: Step; maxHandoffs?: number; model?: Model; }): Pattern

Agents hand the work to each other with no central coordinator. Each agent is told who it can hand off to and ends its reply with HANDOFF: <name> to pass the work on or FINAL when the answer is ready. start names the first agent (or is a dispatcher step that replies with a name); default: the first. Ends on FINAL or after maxHandoffs (default 6, at most 50); a hand-off to an unknown name ends the run with stopReason "unknown_handoff".

Parameters

  • agents (Record<string, Step>): At least two named steps, { name: step }.
  • options ({ start?: Step; maxHandoffs?: number; model?: Model; }, default {}): start: the first agent's name, or a dispatcher step that replies with a name. maxHandoffs (default 6, at most 50). model for specialist steps.

Raises

  • Error: Fewer than two agents.
  • RangeError: maxHandoffs is not a whole number of at least 1.

Example

import { Agent, FakeModel, patterns } from "criogenai";

const triage = new Agent({ model: new FakeModel(["This is a billing question.\nHANDOFF: billing"]) });
const billing = new Agent({ model: new FakeModel(["Your invoice was sent on Monday.\nFINAL"]) });
const result = await patterns.swarm({ triage, billing }).run("Where is my invoice?");
console.log(result.stopReason, result.trace.map((t) => t.step)); // "final" ["triage", "billing"]

humanReview#

function humanReview(step: Step, options?: { model?: Model; }): Pattern

Runs a step, then pauses for a person to look at its draft. Resume with "approve", { decision: "edit", text } or { decision: "reject" }. With compile(storage) the pause is durable: another process can resume it days later.

While paused, waitingFor is { draft, question }.

Parameters

  • step (Step): The step that writes the draft.
  • options ({ model?: Model; }, default {}): model for a specialist step.

Example

import { patterns } from "criogenai";

const flow = patterns.humanReview((text: string) => `Reply: ${text}`);
const paused = await flow.run("Thanks for your order.");
console.log(paused.suspended, paused.waitingFor); // true { draft: "Reply: ...", question: "..." }
const done = await flow.resume(paused, { decision: "edit", text: "Thank you for your order!" });
console.log(String(done), done.stopReason); // "Thank you for your order!" "approved"

quick#

function quick(spec: string, options?: { model?: Model; }): Pattern

A pattern from one line of registered specialist names (plug and play):

quick("researcher > writer")   one after the other
quick("sentiment | keywords")  at the same time
quick("writer <-> critic")     write, review, revise until approved
quick("summarizer")            a single specialist

One kind of connector per line (combine patterns with the functions above).

The specialists need a model: options.model, or the CRIOGEN_MODEL environment variable.

Parameters

  • spec (string): The line, e.g. "researcher > writer".
  • options ({ model?: Model; }, default {}): model: a model or a model string such as "openai:gpt-5-mini".

Raises

  • Error: The line is empty, mixes connectors, has an empty name, or uses <-> with other than two names; a name is not registered; or there is no model.

Example

import { patterns } from "criogenai";

// Needs OPENAI_API_KEY.
const flow = patterns.quick("researcher > writer", { model: "openai:gpt-5-mini" });
const result = await flow.run("Write about solar panels");
console.log(String(result));
console.log(result.explain());

react#

function react(model: Model, tools: (AgentTool | Tool<any>)[], options?: AgentExtras & { maxSteps?: number; instructions?: string; }): Agent<any>

A single agent in a reason-act loop with a hard cap on model calls (default 8, at most 50).

Returns an Agent, so it can be a step of another pattern.

Parameters

  • model (Model): The agent's model.
  • tools ((AgentTool | Tool<any>)[]): The tools it may call.
  • options (AgentExtras & { maxSteps?: number; instructions?: string; }, default {}): Any other Agent option, plus maxSteps (the cap on model calls) and instructions (put before the reason-act instructions).

Raises

  • RangeError: maxSteps is not a whole number of at least 1.

Example

import { FakeModel, FakeReply, patterns, tool } from "criogenai";

const lookup = tool({
  name: "order_status",
  description: "Status of an order.",
  input: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
  run: async ({ id }: { id: string }) => ({ id, status: "shipped" }),
});
const model = new FakeModel([FakeReply.toolCall("order_status", { id: "A-1" }), "Order A-1 has shipped."]);
const agent = patterns.react(model, [lookup], { maxSteps: 4 });
console.log((await agent.run("Where is order A-1?")).text);

coordinator#

function coordinator(model: Model, team: Record<string, string | Agent<any> | Specialist | [string | Agent<any> | Specialist, string]>, options?: AgentExtras & { instructions?: string; }): Agent<any>

An agent that reads each request and hands it to the right specialist, which it calls as a tool. specialists: { name: Agent | Specialist | "registered-name" | [Agent, "when to use it"] }. A model decides the routing (more flexible than a fixed workflow, and more model calls).

Returns an Agent; each specialist becomes one of its tools.

Parameters

  • model (Model): The coordinator's model (also used to build specialists given as a Specialist or a name).
  • team (Record<string, string | Agent<any> | Specialist | [string | Agent<any> | Specialist, string]>): The specialists by tool name. A description (from the pair, or the Specialist's own) tells the model when to call each.
  • options (AgentExtras & { instructions?: string; }, default {}): Any other Agent option, plus instructions to replace the default coordinator instructions.

Raises

  • Error: The team is empty, or a name is not a registered specialist.
  • TypeError: A team member is not an Agent, a Specialist or its name.

Example

import { Agent, FakeModel, FakeReply, patterns } from "criogenai";

const billing = new Agent({ model: new FakeModel(["Your invoice was sent on Monday."]) });
const lead = patterns.coordinator(
  new FakeModel([FakeReply.toolCall("billing", { input: "Where is my invoice?" }), "It was sent on Monday."]),
  { billing: [billing, "Invoices and payments"] },
);
console.log((await lead.run("Where is my invoice?")).text);

hierarchical#

function hierarchical(model: Model, tree: Record<string, TreeNode>, options?: AgentExtras & { instructions?: string; }): Agent<any>

A coordinator of coordinators: the root splits the work, each level hands it further down, workers do it. tree maps names to a worker (as for coordinator) or to a nested object, which becomes a sub-coordinator (describe it with a _description key).

Returns the root coordinator, an Agent.

Parameters

  • model (Model): The model of every coordinator (and of specialists given as a Specialist or a name).
  • tree (Record<string, TreeNode>): Names mapped to workers or to nested objects (sub-teams).
  • options (AgentExtras & { instructions?: string; }, default {}): Any other Agent option for the root, plus its instructions.

Example

import { patterns } from "criogenai";

// Needs OPENAI_API_KEY; "researcher" and "writer" are registered specialists.
const lead = patterns.hierarchical("openai:gpt-5-mini", {
  research: { _description: "Finds facts", web: "researcher" },
  writing: { _description: "Writes the text", writer: "writer" },
});
console.log((await lead.run("A one-page brief on heat pumps")).text);