Quickstart
Pick your language once in any tab: the whole site follows. Each sample is compiled or type-checked against the current SDK, so it runs as shown.
1. An agent with a tool#
A tool is a function the model may call. Its arguments are checked against
the tool's schema before your code runs; a ToolError goes back to the
model, which can try again or explain.
from criogenai import Agent, ToolError, tool
@tool
async def get_order(order_id: str) -> dict:
"""Look up an order by id."""
if order_id != "A-1001":
raise ToolError(f"No order {order_id}") # the model sees this and can recover
return {"status": "shipped", "eta": "tomorrow"}
agent = Agent(
"openai:gpt-5-mini", # key from OPENAI_API_KEY
instructions="You are a support agent. Be brief.",
tools=[get_order],
)
async def main() -> None:
result = await agent.run("Where is order A-1001?")
print(result.text)
asyncio.run(main())import { Agent, ToolError, tool } from "criogenai";
const getOrder = tool({
name: "get_order",
description: "Look up an order by id.",
input: {
type: "object",
properties: { orderId: { type: "string" } },
required: ["orderId"],
},
run: async ({ orderId }) => {
if (orderId !== "A-1001") throw new ToolError(`No order ${orderId}`); // the model sees this
return { status: "shipped", eta: "tomorrow" };
},
});
const agent = new Agent({
model: "openai:gpt-5-mini", // key from OPENAI_API_KEY
instructions: "You are a support agent. Be brief.",
tools: [getOrder],
});
const result = await agent.run("Where is order A-1001?");
console.log(result.text);use criogenai_agent::{Agent, ToolError, tool_fn};
use criogenai_providers::{Endpoint, ProviderModel, ToolSpec};
use serde_json::json;
let model = ProviderModel::new(
Endpoint::openai(std::env::var("OPENAI_API_KEY")?),
"gpt-5-mini",
)?;
let get_order = tool_fn(
ToolSpec::new(
"get_order",
"Look up an order by id.",
json!({
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"]
}),
),
|args, _ctx| async move {
match args["order_id"].as_str() {
Some("A-1001") => Ok(json!({"status": "shipped", "eta": "tomorrow"})),
other => Err(ToolError::new(format!("No order {other:?}"))),
}
},
);
let agent = Agent::builder(model)
.instructions("You are a support agent. Be brief.")
.tool(get_order)
.build()?;
let result = agent.run("Where is order A-1001?").await?;
println!("{}", result.text().unwrap_or_default());2. A conversation that remembers#
A thread is one conversation, saved in storage after every step. The next message continues it, from this process or another.
chat = agent.thread("user-42")
await chat.send("Hi, I am Ada")
reply = await chat.send("What is my name?") # remembers the conversation
history = await chat.history(20) # checkpoints, newest first
await chat.rewind(history[1]["id"]) # go back one step
# After a crash mid-run: continue where it stopped.
# Finished tool calls are not run again.
await chat.retry()
await chat.delete()const chat = agent.thread("user-42");
await chat.send("Hi, I am Ada");
const reply = await chat.send("What is my name?"); // remembers the conversation
const history = await chat.history(20); // checkpoints, newest first
await chat.rewind(String(history[1].id)); // go back one step
// After a crash mid-run: continue where it stopped.
// Finished tool calls are not run again.
await chat.retry();
// Every call takes an AbortSignal: cancelling stops the run, and it is still saved.
await chat.send("Long question", { signal: AbortSignal.timeout(30_000) });
await chat.delete();let chat = agent.thread("user-42");
chat.send("Hi, I am Ada").await?;
let reply = chat.send("What is my name?").await?; // remembers the conversation
let history = chat.history(20).await?; // checkpoints, newest first
chat.rewind(&history[1].id).await?; // go back one step
// After a crash mid-run: continue where it stopped.
// Finished tool calls are not run again.
chat.retry().await?;
chat.delete().await?;3. A person approves first#
Name the tools that need approval. The run pauses before any of them runs and waits in storage until someone approves, rejects or edits the call.
from criogenai import Agent, Decision, Storage
agent = Agent(
"openai:gpt-5-mini",
tools=[issue_refund],
approve=["issue_refund"], # a person decides before it runs
storage=Storage("sqlite://support.db"),
)
chat = agent.thread("ticket-7", actor="user:ada")
reply = await chat.send("Order A-1001 arrived broken, please refund it")
if reply.needs_approval:
for request in reply.approvals:
print(request.tool, request.args)
# Hours later, in another process: the approval waited in the database.
reply = await agent.thread("ticket-7", actor="user:dr-rao").approve()
# Or: await chat.reject("Refund policy says no")
# await chat.decide({call_id: Decision.edit({"order_id": "A-1001", "amount": 12})})import { Agent, Decision, Storage } from "criogenai";
const agent = new Agent({
model: "openai:gpt-5-mini",
tools: [issueRefund],
approve: ["issue_refund"], // a person decides before it runs
storage: new Storage("sqlite://support.db"),
});
const chat = agent.thread("ticket-7", { actor: "user:ada" });
let reply = await chat.send("Order A-1001 arrived broken, please refund it");
if (reply.needsApproval) {
for (const request of reply.approvals) console.log(request.tool, request.args);
}
// Hours later, in another process: the approval waited in the database.
reply = await agent.thread("ticket-7", { actor: "user:dr-rao" }).approve();
// Or: await chat.reject("Refund policy says no")
// await chat.decide({ [callId]: Decision.edit({ orderId: "A-1001", amount: 12 }) })use criogenai_agent::{Agent, Approval};
use criogenai_storage::Storage;
let agent = Agent::builder(model)
.tool(refund)
.approval(Approval::new().tool("issue_refund"))
.storage(Storage::connect("sqlite://support.db").await?)
.build()?;
let chat = agent.thread("ticket-7").actor("user:ada");
let reply = chat
.send("Order A-1001 arrived broken, please refund it")
.await?;
if reply.needs_approval() {
for request in reply.approvals() {
println!(
"{} wants {}",
request.tool,
serde_json::Value::Object(request.args)
);
}
}
// Hours later, in another process: the approval waited in the database.
let reply = agent
.thread("ticket-7")
.actor("user:dr-rao")
.approve()
.await?;
// Or .reject("Refund policy says no"), or one decision per call:
// .decide([(call_id, Decision::edit(new_args))]) / Decision::respond("Already refunded").4. Testing without a model#
FakeModel plays back scripted replies: no network, no cost, the same
result every time. Use it in your tests.
from criogenai import Agent, FakeModel, FakeReply, ToolContext, ToolError, tool
@tool(destructive=True)
def issue_refund(order_id: str, amount: float, note: Optional[str] = None, ctx: Optional[ToolContext] = None) -> dict:
"""Refund an order."""
# Arguments arrive already checked against the schema built from the type hints.
if amount > 200:
raise ToolError("Refunds over 200 need a manager")
return {"refunded": amount}
class Resolution(BaseModel):
category: Literal["billing", "shipping", "other"]
refund: bool
agent = Agent(
"openai:gpt-5-mini",
instructions="You are a support agent.",
tools=[issue_refund],
output=Resolution, # result.output is a Resolution
limits={"model_calls": 10, "tool_calls": 20},
)
# In tests: scripted replies, no network, the same answer every time.
test_agent = Agent(
FakeModel([FakeReply.tool_call("issue_refund", {"order_id": "A-1001", "amount": 24}), "Refunded."]),
tools=[issue_refund],
)
result = test_agent.run_sync("Refund my order")
assert result.text == "Refunded."import { Agent, FakeModel, FakeReply, ToolError, tool } from "criogenai";
const issueRefund = tool<{ orderId: string; amount: number }>({
name: "issue_refund",
description: "Refund an order.",
destructive: true,
input: {
type: "object",
properties: {
orderId: { type: "string" },
amount: { type: "number", minimum: 0, maximum: 500 },
},
required: ["orderId", "amount"],
},
run: ({ amount }) => {
// Arguments arrive already checked against the schema.
if (amount > 200) throw new ToolError("Refunds over 200 need a manager");
return { refunded: amount };
},
});
const agent = new Agent<{ category: string; refund: boolean }>({
model: "openai:gpt-5-mini",
instructions: "You are a support agent.",
tools: [issueRefund],
output: {
type: "object",
properties: { category: { enum: ["billing", "shipping", "other"] }, refund: { type: "boolean" } },
required: ["category", "refund"],
},
limits: { modelCalls: 10, toolCalls: 20 },
});
// In tests: scripted replies, no network, the same answer every time.
const testAgent = new Agent({
model: new FakeModel([FakeReply.toolCall("issue_refund", { orderId: "A-1001", amount: 24 }), "Refunded."]),
tools: [issueRefund],
});
const result = await testAgent.run("Refund my order");Next#
- Tutorial: build a support agent: all of this in one small app, step by step.
- Agents and Tools: options, typed output, errors.
- Conversations and streaming.
- Storage: SQLite and Postgres.
- Encryption, audit and retention for sensitive data.