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 orderimport { 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 orderuse 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 orderOne line, one connector:
| Line | Pattern |
|---|---|
a > b > c | Sequential: each output feeds the next |
a | b | c | Parallel: at the same time, combined in order |
writer <-> critic | Review and critique: revise until the critic approves |
summarizer | A 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 need | Use |
|---|---|
| Tools and a model, nothing more | Agent |
| A fixed order of steps | sequential |
| Independent work, faster | parallel |
| Better quality before use | review_and_critique (reviewAndCritique in TypeScript) |
| Repeat until a condition | loop, iterative_refinement (iterativeRefinement) |
| Requests of many kinds, routed by a model | coordinator |
| Big ambiguous problems, split level by level | hierarchical |
| Agents that debate and hand work over | swarm |
| Reason, act, observe with a hard cap | react |
| A person approves a draft | human_review (humanReview) |
| Branches no pattern fits | custom(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),
)?;Web search#
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 edgesimport { 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 edgeslet 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 graphEvery pattern here is exercised on the demo page, with a scripted model and with a real one (the demo runs the Python form).