Patterns, specialists and web search

Most multi-agent systems are one of a dozen shapes. CriogenAI ships each shape as a function, so you pick one instead of wiring it. They are built from the agents and workflows you already know, so you can inspect them, run them with durable threads, and extend them. The same shapes, names and limits exist in Python, TypeScript and Rust.

Plug and play#

from criogenai import patterns

# A model for the specialists: pass one, or set CRIOGEN_MODEL="openai:gpt-5-mini".
flow = patterns.quick("researcher > writer", model="openai:gpt-5-mini")
result = await flow.run("Write about solar panels")
print(result)             # the answer
print(result.explain())   # each step, in order
import { patterns } from "criogenai";

// A model for the specialists: pass one, or set CRIOGEN_MODEL="openai:gpt-5-mini".
const flow = patterns.quick("researcher > writer", { model: "openai:gpt-5-mini" });
const result = await flow.run("Write about solar panels");
console.log(String(result));      // the answer
console.log(result.explain());    // each step, in order
use criogenai_agent::patterns;

// Specialists need a model: pass an `Arc<dyn Model>` (Rust has no model strings).
let flow = patterns::quick("researcher > writer", model.clone())?;
let result = flow.run("Write about solar panels").await?;
println!("{result}");              // the answer
println!("{}", result.explain());  // each step, in order

One line, one connector:

LinePattern
a > b > cSequential: each output feeds the next
a | b | cParallel: at the same time, combined in order
writer <-> criticReview and critique: revise until the critic approves
summarizerA single specialist

Choosing a pattern#

Start with the simplest that works. A task a single model call can do needs no agent at all.

You needUse
Tools and a model, nothing moreAgent
A fixed order of stepssequential
Independent work, fasterparallel
Better quality before usereview_and_critique (reviewAndCritique in TypeScript)
Repeat until a conditionloop, iterative_refinement (iterativeRefinement)
Requests of many kinds, routed by a modelcoordinator
Big ambiguous problems, split level by levelhierarchical
Agents that debate and hand work overswarm
Reason, act, observe with a hard capreact
A person approves a drafthuman_review (humanReview)
Branches no pattern fitscustom(workflow)

Patterns that rely on a model to decide (coordinator, hierarchical, swarm) make more model calls than the fixed ones (sequential, parallel, loop).

Every loop ends#

Loops, review rounds and swarm hand-offs all have a cap with a default and a hard ceiling of 50. A run reports why it stopped (stop_reason, stopReason in TypeScript, PatternStop in Rust): done, condition, approved, max_iterations, max_rounds, final, max_handoffs, unknown_handoff (a swarm agent named someone who is not in the team), rejected or suspended.

Steps#

A step can be an agent, a specialist or its registered name, a function text -> text (sync or async), or a (name, step) pair to name it in the trace.

def shout(text):                  # a plain function is a step too
    return text.upper()

flow = patterns.sequential([("research", my_agent), shout, "writer"], model=m)
const shout = (text: string) => text.toUpperCase();   // a plain function is a step too

const flow = patterns.sequential([["research", myAgent], shout, "writer"], { model: m });
let shout = Step::function(|text: String| async move { text.to_uppercase() });

let flow = patterns::sequential([
    Step::from(("research", my_agent)),
    shout.named("shout"),
    Step::registered("writer", model.clone())?,
])?;

Specialists#

A specialist is a recipe for an agent: a name, a description, instructions and default settings. Built in: researcher, summarizer, classifier, extractor, planner, writer, critic, fact_checker, code_reviewer, translator, support_agent. They are prompts and defaults; you add the tools.

from criogenai import specialists

agent = specialists.writer.build("openai:gpt-5-mini")

# Extend: a new recipe; the original is untouched.
careful = specialists.researcher.extend("careful-researcher", add="Always give two sources.")
specialists.register(careful)          # now "careful-researcher" works anywhere a step is expected

# Or write your own.
specialists.register(specialists.Specialist(
    "refund-triage", "Decides if a refund request is valid", "You decide whether ...", tools=[get_order],
))
import { specialists } from "criogenai";

const agent = specialists.writer.build("openai:gpt-5-mini");

// Extend: a new recipe; the original is untouched.
const careful = specialists.researcher.extend("careful-researcher", { add: "Always give two sources." });
specialists.register(careful);         // now "careful-researcher" works anywhere a step is expected

// Or write your own.
specialists.register(new specialists.Specialist(
  "refund-triage", "Decides if a refund request is valid", "You decide whether ...", { tools: [getOrder] },
));
use criogenai_agent::specialists::{self, Specialist};

let agent = specialists::writer().build(model.clone())?;

// Extend: a new recipe; the original is untouched.
let careful = specialists::researcher()
    .extend("careful-researcher")
    .append_instructions("Always give two sources.");
specialists::register(careful)?;       // now "careful-researcher" works anywhere a step is expected

// Or write your own.
specialists::register(
    Specialist::new("refund-triage", "Decides if a refund request is valid", "You decide whether ...")?
        .tool(get_order),
)?;
from criogenai import Agent, web

agent = Agent("openai:gpt-5-mini", tools=[web.search(), web.fetch()])
import { Agent, web } from "criogenai";

const agent = new Agent({ model: "openai:gpt-5-mini", tools: [web.search(), web.fetch()] });
// Cargo feature `web` (off by default): criogenai-agent = { version = "0.1", features = ["web"] }
use criogenai_agent::web;

let agent = Agent::builder(model)
    .tool(web::search()?)
    .tool(web::fetch())
    .build()?;

search() needs no arguments: it uses TAVILY_API_KEY, else BRAVE_API_KEY (tavily.com, brave.com/search/api), and tells you what to set if neither is. The researcher specialist gets search and page reading automatically when a key is set.

  • Results are bounded: a few results with short snippets, pages cut to a fixed size.
  • The key is sent only as a request header over https, and never appears in a tool result, an error or a log.
  • fetch() reads public pages only: private-network addresses are refused in every spelling (including IPv4 inside IPv6), a model must not reach your internal services through it, redirects are not followed, and at most 500 KB is read.

Make it durable, extend it#

A pattern is a normal workflow underneath, so it gets threads, approvals, an audit trail and storage like any other.

from criogenai import Storage

app = patterns.human_review(my_agent).compile(Storage("sqlite://app.db"))
await app.thread("release-1", actor="user:ada").run({"input": "Release the dataset?"})
# Days later, from another process:
await app.thread("release-1", actor="user:boss").resume({"review": "approve"})

flow.workflow.add_node(...)   # the plain Workflow, to add nodes and edges
import { Storage } from "criogenai";

const app = patterns.humanReview(myAgent).compile(new Storage({ url: "sqlite://app.db" }));
await app.thread("release-1", { actor: "user:ada" }).run({ input: "Release the dataset?" });
// Days later, from another process:
await app.thread("release-1", { actor: "user:boss" }).resume({ review: "approve" });

flow.workflow.addNode(/* ... */);   // the plain Workflow, to add nodes and edges
let flow = patterns::human_review(Step::from(my_agent))?;
let thread = flow.thread(&storage, "release-1");
// ... run it, and later, from another process:
let done = flow.resume(&paused, ReviewDecision::Approve).await?;

let flow = flow.extend(|workflow| { /* add nodes and edges */ })?;   // rebuilds and revalidates the graph

Every pattern here is exercised on the demo page, with a scripted model and with a real one (the demo runs the Python form).