Demo
Every feature, run for real
Each scenario below is ordinary CriogenAI code. It was run, and the page shows that exact code, what it printed and the checks it had to pass. Chat with the agent yourself, or read the recorded tours: one with a scripted model, so it is exact and repeatable, and one where a real model decides.
Nothing here is mocked at the SDK level: the Rust engine, storage, approvals, hooks and audit trail are the real ones. Results are recorded files you can regenerate yourself (see the end of the page).
Try it
Looking for the local demo server…
27 of 27 scenarios passed. A scripted model gives the same replies every time, so every scenario is exact and repeatable. criogen 0.1.0 (Python), Windows 11, 2026-10-24.
Agents
✓Agents with tools, limits and typed output
Python functions become tools, the model's calls run concurrently, and the answer can be a checked structure.
Checks
- both tools ran and the model got both results
- the output is the checked structure
- the limit stopped it with a stable error code
What it printed
- schema built from the type hints
{ "type": "object", "properties": { "a": { "type": "integer" }, "b": { "type": "integer" } }, "required": [ "a", "b" ], "additionalProperties": false }- answer
5 and 8
- tool results, in call order
[ 5, { "call_id": "call_1_1", "value": 8 } ]- typed output
{ "category": "billing", "refund": true }- a run that loops
limit_exceeded: the agent reached its model_calls limit (2)
The code that ran
async def agents():
@tool
def add(a: int, b: int) -> int:
"""Adds two numbers."""
return a + b
@tool
async def double(n: int, ctx: ToolContext) -> dict:
"""Doubles a number."""
await asyncio.sleep(0.01)
return {"value": n * 2, "call_id": ctx.call_id}
show("schema built from the type hints", add.schema)
agent = Agent(
FakeModel([FakeReply.tool_calls([("add", {"a": 2, "b": 3}), ("double", {"n": 4})]), "5 and 8"]),
tools=[add, double],
)
result = await agent.run("go")
show("answer", result.text)
show("tool results, in call order", [m["content"][0]["value"] for m in tool_results(result)])
check("both tools ran and the model got both results", result.tool_calls == 2 and result.text == "5 and 8")
schema = {
"type": "object",
"properties": {"category": {"type": "string"}, "refund": {"type": "boolean"}},
"required": ["category", "refund"],
"additionalProperties": False,
}
typed = Agent(FakeModel([FakeReply.tool_call("final_output", {"category": "billing", "refund": True})]), output=schema)
result = await typed.run("I was charged twice")
show("typed output", result.output)
check("the output is the checked structure", result.output == {"category": "billing", "refund": True})
looping = Agent(FakeModel([FakeReply.tool_call("add", {"a": 1, "b": 1})] * 5), tools=[add], limits={"model_calls": 2})
try:
await looping.run("loop")
check("a looping agent is stopped", False)
except LimitExceededError as error:
show("a run that loops", f"{error.code}: {error.message}")
check("the limit stopped it with a stable error code", error.code == "limit_exceeded")✓Arguments are checked before your code runs
A model's malformed call never reaches the tool; the model is told what was wrong and can recover.
Checks
- the tool function never ran
- the model received an 'Invalid arguments' error
What it printed
- what the model was told
[{'text': 'Error: Invalid arguments: amount: expected integer, got "lots"', 'type': 'text'}]
The code that ran
async def validation():
calls = []
@tool
def charge(amount: int) -> str:
"""Charges a card."""
calls.append(amount)
return "ok"
agent = Agent(FakeModel([FakeReply.tool_call("charge", {"amount": "lots"}), "I could not charge that."]), tools=[charge])
result = await agent.run("charge")
message = str(tool_results(result)[0]["content"])
show("what the model was told", message)
check("the tool function never ran", calls == [])
check("the model received an 'Invalid arguments' error", "Invalid arguments" in message)✓Tool failures go back to the model, on your terms
A ToolError or an unexpected exception reaches the model by default; you can hide the details or fail the run.
Checks
- the model got the message and answered
- the crash is reported with a stable code
- the exception text stays out of the model's view
- the original exception is the cause
What it printed
- ToolError: the model sees
[{'text': 'Error: No such order', 'type': 'text'}]- unexpected exception (default)
[{'text': "Error: The tool failed with an unexpected error: KeyError: 'x'", 'type': 'text'}]- return_generic: the model sees
[{'text': 'Error: The tool failed with an unexpected error.', 'type': 'text'}]- fail: the run raises
ToolExecutionError (cause: KeyError)
The code that ran
async def tool_errors():
@tool
def lookup(order_id: str) -> str:
"""Looks up an order."""
if order_id == "missing":
raise ToolError("No such order", retryable=False)
raise KeyError(order_id)
result = await Agent(FakeModel([FakeReply.tool_call("lookup", {"order_id": "missing"}), "Sorry, not found."]), tools=[lookup]).run("where")
show("ToolError: the model sees", str(tool_results(result)[0]["content"]))
check("the model got the message and answered", result.text == "Sorry, not found.")
script = [FakeReply.tool_call("lookup", {"order_id": "x"}), "The order system failed."]
result = await Agent(FakeModel(script), tools=[lookup]).run("go")
crash = tool_results(result)[0]
show("unexpected exception (default)", str(crash["content"]))
check("the crash is reported with a stable code", crash["error"]["code"] == "tool_failed")
quiet = await Agent(FakeModel(script), tools=[lookup], on_tool_exception="return_generic").run("go")
hidden = tool_results(quiet)[0]
show("return_generic: the model sees", str(hidden["content"]))
check("the exception text stays out of the model's view", "KeyError" not in str(hidden["content"]))
try:
await Agent(FakeModel(script), tools=[lookup], on_tool_exception="fail").run("go")
check("fail ends the run", False)
except criogen.ToolExecutionError as error:
show("fail: the run raises", f"{type(error).__name__} (cause: {type(error.__cause__).__name__})")
check("the original exception is the cause", isinstance(error.__cause__, KeyError))✓Agents as tools of other agents
An agent becomes a tool of another, so you can build teams without a separate orchestration layer.
Checks
- the writer used the researcher's answer
What it printed
- what the researcher told the writer
[{'text': 'The order shipped on Monday.', 'type': 'text'}]- final answer
It shipped on Monday.
The code that ran
async def agents_compose():
researcher = Agent(FakeModel(["The order shipped on Monday."]), instructions="Find facts.")
writer = Agent(
FakeModel([FakeReply.tool_call("research", {"input": "When did A-1 ship?"}), "It shipped on Monday."]),
tools=[researcher.as_tool("research", "Looks things up")],
)
result = await writer.run("When did my order ship?")
show("what the researcher told the writer", str(tool_results(result)[0]["content"]))
show("final answer", result.text)
check("the writer used the researcher's answer", "Monday" in result.text)Human in the loop
✓Approve, reject or edit a call before it runs
A risky tool pauses the run in the database; a different process decides later, and the decision is applied exactly.
Checks
- the refund has not run
- it ran once, after the approval
- a rejected call never runs
- the edited arguments were used
What it printed
- the run stops and asks
[ { "tool": "refund", "args": { "amount": 24, "order_id": "A-1" } } ]- sending while it waits
thread_suspended
- approved by user:boss
Refunded.
- rejected: the model is told why
I did not refund it.
- edited from 500 to 12; what ran
[ "A-3", 12 ]
The code that ran
async def approvals():
storage = Storage()
refunds = []
@tool(destructive=True)
def refund(order_id: str, amount: float) -> str:
"""Refunds an order."""
refunds.append((order_id, amount))
return "refunded"
def make(replies):
return Agent(FakeModel(replies), tools=[refund], approve=["refund"], storage=storage)
chat = make([FakeReply.tool_call("refund", {"order_id": "A-1", "amount": 24})]).thread("t-1", actor="user:ada")
reply = await chat.send("Refund A-1")
show("the run stops and asks", [{"tool": r.tool, "args": r.args} for r in reply.approvals])
check("the refund has not run", reply.needs_approval and refunds == [])
try:
await chat.send("hello?")
check("a waiting thread refuses new messages", False)
except ThreadSuspendedError as error:
show("sending while it waits", error.code)
other = make(["Refunded."]).thread("t-1", actor="user:boss") # another process, in real life
reply = await other.approve()
show("approved by user:boss", reply.text)
check("it ran once, after the approval", refunds == [("A-1", 24)])
# Reject
agent = make([FakeReply.tool_call("refund", {"order_id": "A-2", "amount": 9}), "I did not refund it."])
chat = agent.thread("t-2")
await chat.send("Refund A-2")
reply = await chat.reject("Policy says no")
show("rejected: the model is told why", reply.text)
check("a rejected call never runs", ("A-2", 9) not in refunds)
# Edit
agent = make([FakeReply.tool_call("refund", {"order_id": "A-3", "amount": 500}), "Refunded the smaller amount."])
chat = agent.thread("t-3")
reply = await chat.send("Refund A-3")
call_id = reply.approvals[0].call_id
await chat.decide({call_id: Decision.edit({"order_id": "A-3", "amount": 12})})
show("edited from 500 to 12; what ran", refunds[-1])
check("the edited arguments were used", refunds[-1] == ("A-3", 12))Conversations
✓Saved conversations: history, rewind, delete
Every step is a checkpoint you can list, go back to, and delete; a new agent object continues the same conversation.
Checks
- the second agent continued the conversation from disk
- rewinding creates a fork
- delete reports it deleted something
What it printed
- messages the model saw
[ "user", "assistant", "user", "assistant" ]
- checkpoints, newest first
[ { "status": "completed" }, { "status": "in_progress" }, { "status": "completed" }, { "status": "in_progress" } ]- rewind forks from the oldest
fork
The code that ran
async def threads():
path = temp_db("threads")
try:
first = Agent(FakeModel(["Hello Ada"]), storage=Storage(f"sqlite:///{path}"))
await first.thread("t").send("I am Ada")
await first.storage.close()
storage = Storage(f"sqlite:///{path}")
second = Agent(FakeModel(["You are Ada"]), storage=storage) # a new agent and connection
chat = second.thread("t")
reply = await chat.send("Who am I?")
show("messages the model saw", [m["role"] for m in reply.messages])
check("the second agent continued the conversation from disk", len(reply.messages) == 4)
history = await chat.history(20)
show("checkpoints, newest first", [{"status": h["status"]} for h in history])
rewound = await chat.rewind(history[-1]["id"])
show("rewind forks from the oldest", rewound["source"])
check("rewinding creates a fork", rewound["source"] == "fork")
check("delete reports it deleted something", await chat.delete() is True)
await storage.close()
finally:
remove_db(path)✓Crash recovery without repeating finished tools
When a run fails mid-way, retry() continues from the last saved step; tools that finished are not run again.
Checks
- the run finished after the retry
- fetch was not run a second time
What it printed
- first attempt
tool_failed: tool `send` failed: RuntimeError: the mail server is down
- tool calls so far
{ "fetch": 1, "send": 1 }- after retry()
Done.
- tool calls in total
{ "fetch": 1, "send": 2 }
The code that ran
async def crash_recovery():
counts = {"fetch": 0, "send": 0}
broken = {"on": True}
@tool
def fetch(order_id: str) -> str:
"""Fetches an order."""
counts["fetch"] += 1
return "order A-1: shipped"
@tool
def send(text: str) -> str:
"""Sends an email."""
counts["send"] += 1
if broken["on"]:
raise RuntimeError("the mail server is down")
return "sent"
agent = Agent(
FakeModel([FakeReply.tool_calls([("fetch", {"order_id": "A-1"})]), FakeReply.tool_call("send", {"text": "hi"}), "Done."]),
tools=[fetch, send],
storage=Storage(),
on_tool_exception="fail",
)
chat = agent.thread("job")
try:
await chat.send("Look it up and email the customer")
check("the first attempt fails", False)
except criogen.CriogenError as error:
show("first attempt", f"{error.code}: {error.message}")
show("tool calls so far", dict(counts))
broken["on"] = False
reply = await chat.retry()
show("after retry()", reply.text)
show("tool calls in total", dict(counts))
check("the run finished after the retry", reply.text == "Done.")
check("fetch was not run a second time", counts["fetch"] == 1)✓Streaming and cancellation
Text and tool events arrive as they happen; cancelling stops the model call and the thread is still saved.
Checks
- the streamed text equals the saved reply
- an approval request is a stream event
- cancellation is prompt
What it printed
- events
[ "other", "other", "other", "other", "text", "message", "other", "other", "other" ]
- cancel() on a 5 s model call returned after
3 ms
The code that ran
async def streaming():
@tool
def add(a: int, b: int) -> int:
"""Adds."""
return a + b
agent = Agent(FakeModel(["Hello there", FakeReply.tool_call("add", {"a": 1, "b": 2}), "Three."]), tools=[add], approve=["add"])
chat = agent.thread("s")
stream = chat.stream("hi")
events = [e async for e in stream]
show("events", [e.type for e in events])
text = "".join(e.text for e in events if e.type == "text")
check("the streamed text equals the saved reply", text == "Hello there" == (await stream.reply()).text)
stream = chat.stream("add")
types = [e.type async for e in stream]
check("an approval request is a stream event", "approval_needed" in types)
await stream.reply()
slow = Agent(FakeModel(["never"], delay=5.0))
stream = slow.thread("x").stream("hi")
await asyncio.sleep(0.2)
started = time.perf_counter()
stream.cancel()
try:
await stream.reply()
except criogen.CancelledError:
took = time.perf_counter() - started
show("cancel() on a 5 s model call returned after", f"{took * 1000:.0f} ms")
check("cancellation is prompt", took < 1.0)Storage and compliance
✓SQLite, Postgres and in-memory storage
The same agent code saves to a file, to Postgres, or to memory; here SQLite and memory.
Checks
- the conversation is on disk
What it printed
- SQLite file written
76 KB
- in-memory storage
kept in memory
- Postgres
the same Storage('postgres://…') call; its tests run in CI against a real server (not part of this tour)
The code that ran
async def storage_backends():
path = temp_db("storage")
try:
storage = Storage(f"sqlite:///{path}")
agent = Agent(FakeModel(["saved"]), storage=storage)
await agent.thread("t").send("hello")
await storage.close()
size = os.path.getsize(path)
show("SQLite file written", f"{size // 1024} KB")
check("the conversation is on disk", size > 0)
finally:
remove_db(path)
memory = Storage()
reply = await Agent(FakeModel(["kept in memory"]), storage=memory).thread("t").send("hi")
show("in-memory storage", reply.text)
show("Postgres", "the same Storage('postgres://…') call; its tests run in CI against a real server (not part of this tour)")✓Encryption at rest
Content in the database is encrypted; without the key the database refuses to open.
Checks
- the secret word is not in the file
- it refuses with a stable code
What it printed
- bytes in the database file
77824
- opening without the key
encryption_key_missing
The code that ran
async def encryption():
path = temp_db("enc")
keys = "k1:" + "ab" * 32
try:
agent = Agent(FakeModel(["secret noted"]), storage=Storage(f"sqlite:///{path}", encryption_keys=keys))
await agent.thread("t").send("my secret is pelican")
await agent.storage.close()
raw = Path(path).read_bytes()
show("bytes in the database file", len(raw))
check("the secret word is not in the file", b"pelican" not in raw)
try:
Storage(f"sqlite:///{path}")
check("opening without the key fails", False)
except StorageError as error:
show("opening without the key", f"{error.code}")
check("it refuses with a stable code", error.code == "encryption_key_missing")
finally:
remove_db(path)✓A tamper-evident audit trail with no content
Runs, model calls, tool calls and decisions are recorded with who did them, in a hash chain, without message text.
Checks
- the chain verifies
- the actors are recorded
- no message content is in any record
What it printed
- records (action, actor)
[ [ "model.called", "user:ada" ], [ "run.finished", "user:ada" ], [ "approval.requested", "user:ada" ], [ "approval.decided", "user:dr-rao" ], [ "tool.executed", "user:dr-rao" ], [ "model.called", "user:dr-rao" ], [ "run.finished", "user:dr-rao" ] ]- hash chain verification
{ "first_broken": null, "head": [ 7, "7203afce6f7df4b4a0e673cde294476ff7e7ef200c040389d49209ff073951eb" ], "ok": true, "records": 7 }
The code that ran
async def audit():
storage = Storage(audit=True)
@tool(destructive=True)
def refund(order_id: str) -> str:
"""Refunds."""
return "done"
agent = Agent(FakeModel([FakeReply.tool_call("refund", {"order_id": "A-1"}), "Refunded."]), tools=[refund], approve=["refund"], storage=storage)
await agent.thread("visit-1", actor="user:ada").send("My name is Ada Lovelace, refund A-1")
await agent.thread("visit-1", actor="user:dr-rao").approve()
records = await storage.audit_records(thread_id="visit-1")
show("records (action, actor)", [(r["action"], r.get("actor")) for r in records])
report = await storage.verify_audit()
show("hash chain verification", report)
check("the chain verifies", report["ok"] is True)
check("the actors are recorded", {"user:ada", "user:dr-rao"} <= {r.get("actor") for r in records})
check("no message content is in any record", "Lovelace" not in json.dumps(records))✓Retention: old data is deleted on a schedule
keep_checkpoints and thread_ttl bound how much is kept; prune() applies them.
Checks
- old checkpoints were deleted
What it printed
- checkpoints before and after prune()
[ 6, 1 ]
- report
{ "checkpoints_deleted": 5, "memory_items_deleted": 0, "threads_busy": 0, "threads_deleted": 0 }
The code that ran
async def retention():
path = temp_db("ret")
try:
storage = Storage(f"sqlite:///{path}", retention={"keep_checkpoints": 1})
agent = Agent(FakeModel(["one", "two", "three"]), storage=storage)
chat = agent.thread("t")
for text in ("a", "b", "c"):
await chat.send(text)
before = len(await chat.history(100))
report = await storage.prune()
after = len(await chat.history(100))
show("checkpoints before and after prune()", [before, after])
show("report", report)
check("old checkpoints were deleted", report["checkpoints_deleted"] > 0 and after < before)
await storage.close()
finally:
remove_db(path)Memory
✓Long-term memory per user
The model can remember, search and forget facts that outlive a conversation; your code can read them too.
Checks
- the model's remember call saved the fact
- a second user has separate memory
- everything about one user can be deleted
What it printed
- stored for users/ada
[ [ "diet", "vegan" ] ]- forget_all (right to erasure)
1 fact deleted
The code that ran
async def memory():
storage = Storage()
agent = Agent(
FakeModel([FakeReply.tool_call("remember", {"key": "diet", "value": "vegan"}), "Noted.", "Eat lentils."]),
storage=storage,
memory=Memory.by_state("user_id", "users"),
)
await agent.run("I am vegan", user_id="ada")
found = await storage.search("users/ada")
show("stored for users/ada", [(f["key"], f["value"]) for f in found])
check("the model's remember call saved the fact", found[0]["value"] == "vegan")
await agent.run("What should I eat?", user_id="ada")
check("a second user has separate memory", await storage.search("users/bob") == [])
deleted = await storage.forget_all("users/ada")
show("forget_all (right to erasure)", f"{deleted} fact deleted")
check("everything about one user can be deleted", deleted == 1)✓Memory by meaning
Facts are found by meaning with embeddings, and by keywords when the embedder is down.
Checks
- found with no shared word, by meaning
- the search still worked
What it printed
- searching 'automobile' (the fact says 'car')
[{'type': 'json', 'value': [{'key': 'vehicle', 'value': 'The customer drives a red car'}]}]- embedder down: falls back to keywords
[{'type': 'json', 'value': [{'key': 'vehicle', 'value': 'The customer drives a red car'}]}]
The code that ran
async def semantic_memory():
storage = Storage()
mem = Memory.shared("facts").semantic(Embedder.fake(synonyms={"automobile": "car"}))
agent = Agent(
FakeModel([FakeReply.tool_call("remember", {"key": "vehicle", "value": "The customer drives a red car"}), "Noted.",
FakeReply.tool_call("search_memory", {"query": "automobile"}), "Found it."]),
storage=storage,
memory=mem,
)
await agent.run("Remember my car")
found = await agent.run("Which automobile do I have?")
result = str(tool_results(found)[0]["content"])
show("searching 'automobile' (the fact says 'car')", result)
check("found with no shared word, by meaning", "red car" in result)
broken = Agent(
FakeModel([FakeReply.tool_call("search_memory", {"query": "red"}), "ok"]),
storage=storage,
memory=Memory.shared("facts").semantic(Embedder.fake(failing=True)),
)
result = str(tool_results(await broken.run("search"))[0]["content"])
show("embedder down: falls back to keywords", result)
check("the search still worked", "red car" in result)Workflows
✓Graphs of steps: routes, parallel branches, joins
A graph with a router, two parallel nodes and a join runs on the Rust engine; every graph error is reported at compile time.
Checks
- both parallel branches ran and joined
- the router sent an empty question elsewhere
What it printed
- state at the end
{ "answer": 3, "docs": 2, "log": [ "classify", "web", "docs" ], "question": "why?", "web": 1 }- steps
4
- compile() of a broken graph
invalid workflow (1 issue(s)) - edge `a` -> `nowhere` refers to unknown node `nowhere`
The code that ran
async def workflows():
wf = Workflow(reducers={"log": "append"})
wf.add_node("classify", lambda s: {"log": ["classify"]})
wf.add_node("fan", lambda s: None)
wf.add_node("search_web", lambda s: {"log": ["web"], "web": 1})
wf.add_node("search_docs", lambda s: {"log": ["docs"], "docs": 2})
wf.add_node("answer", lambda s: {"answer": s["web"] + s["docs"]})
wf.add_node("refuse", lambda s: {"answer": None})
wf.start("classify")
wf.route("classify", lambda s: "ok" if s["question"] else "empty", {"ok": "fan", "empty": "refuse"})
wf.edge("fan", "search_web").edge("fan", "search_docs")
wf.join(["search_web", "search_docs"], "answer")
wf.edge("answer", END).edge("refuse", END)
app = wf.compile()
result = await app.run({"question": "why?"})
show("state at the end", result.state)
show("steps", result.steps)
check("both parallel branches ran and joined", result.state["answer"] == 3 and sorted(result.state["log"]) == ["classify", "docs", "web"])
empty = await app.run({"question": ""})
check("the router sent an empty question elsewhere", empty.state["answer"] is None)
broken = Workflow()
broken.add_node("a", lambda s: None)
broken.start("a").edge("a", "nowhere")
try:
broken.compile()
check("a broken graph is refused", False)
except criogen.CriogenError as error:
show("compile() of a broken graph", error.message)✓Workflows that pause for a person, durably
A node suspends the run; the pause lives in the database and any process can resume it.
Checks
- it is waiting and nothing was paid
- it paid once after the resume
- the audit trail names both people
What it printed
- first run
{ "suspended": true, "state": { "amount": 25, "status": "waiting" } }- what it waits for
[ { "refund": 25 } ]- resumed by user:boss
{ "amount": 25, "approved": true, "paid": true, "status": "decided" }
The code that ran
async def durable_workflows():
storage = Storage(audit=True)
paid = []
def build():
wf = Workflow()
@wf.node
def approve(state, ctx):
if ctx.resume is None:
return suspend({"refund": state["amount"]}, {"status": "waiting"})
return {"approved": ctx.resume == "yes", "status": "decided"}
@wf.node
def pay(state):
if state["approved"]:
paid.append(state["amount"])
return {"paid": state["approved"]}
wf.start("approve").edge("approve", "pay").edge("pay", END)
return wf
thread = build().compile(storage=storage).thread("refund-1", actor="user:ada")
turn = await thread.run({"amount": 25})
show("first run", {"suspended": turn.suspended, "state": turn.state})
show("what it waits for", [i.payload for i in await thread.interrupts()])
check("it is waiting and nothing was paid", turn.suspended and paid == [])
other = build().compile(storage=storage).thread("refund-1", actor="user:boss") # another process, in real life
turn = await other.resume({"approve": "yes"})
show("resumed by user:boss", turn.state)
check("it paid once after the resume", turn.state["paid"] is True and paid == [25])
records = await storage.audit_records(thread_id="refund-1")
check("the audit trail names both people", {"user:ada", "user:boss"} <= {r.get("actor") for r in records})✓Agents as workflow steps
A node can be a whole agent, so a graph mixes plain functions and agents.
Checks
- the agent ran as a node and added its reply
What it printed
- roles in the conversation
[ "user", "assistant" ]
- the agent's reply
Refund approved.
The code that ran
async def agent_nodes():
wf = Workflow(reducers={"messages": "upsert_by_id"})
# An agent node reads and extends the conversation in state["messages"].
question = {"id": "m1", "role": "user", "content": [{"type": "text", "text": "Refund?"}]}
wf.add_node("ask", lambda s: {"messages": [question]})
wf.add_node("agent", Agent(FakeModel(["Refund approved."])))
wf.start("ask").edge("ask", "agent").edge("agent", END)
result = await wf.compile().run({})
show("roles in the conversation", [m["role"] for m in result.state["messages"]])
show("the agent's reply", result.state["messages"][-1]["content"][0]["text"])
check("the agent ran as a node and added its reply", [m["role"] for m in result.state["messages"]] == ["user", "assistant"])Models
✓Retries and fallback models
A transient model error is retried; when a model keeps failing, the next one answers.
Checks
- the retry succeeded
- the fallback model answered
What it printed
- one transient failure, retried
fine now
- primary keeps failing
from the fallback
The code that ran
async def resilience():
result = await Agent(FakeModel(["fine now"], fail_first=1)).run("hello")
show("one transient failure, retried", result.text)
check("the retry succeeded", result.text == "fine now")
agent = Agent(
FakeModel(["never"], fail_first=100),
fallbacks=[FakeModel(["from the fallback"])],
model_retry=Retry(max_attempts=2, initial_backoff=0.01, jitter=False),
)
result = await agent.run("hi")
show("primary keeps failing", result.text)
check("the fallback model answered", result.text == "from the fallback")✓Hooks around model and tool calls
Your code can change arguments, skip a tool, redact a result, replace a reply or end a run, in a fixed order.
Checks
- before_tool changed the argument
- after_tool redacted the card number
- a hook skipped a tool with a cached value
- after_model replaced the reply
- the budget hook stopped it
What it printed
- argument the tool received
[ "A-1" ]
- what the model saw from the tools
[ "[{'text': 'order A-1: shipped, card [card]', 'type': 'text'}]", "[{'text': 'sunny (cached)', 'type': 'text'}]" ]- final answer
[redacted]
- a hook ended the run
Stopped by the budget hook.
The code that ran
async def hooks():
seen = []
@tool
def lookup(order_id: str) -> str:
"""Looks up an order."""
seen.append(order_id)
return f"order {order_id}: shipped, card 4111-1111"
@tool
def weather(city: str) -> str:
"""The weather."""
raise AssertionError("skipped by the hook")
class Guard:
def before_tool(self, call):
if call.name == "weather":
return ToolResult("sunny (cached)") # skip the tool
call.args["order_id"] = call.args["order_id"].upper()
return call
def after_tool(self, call, result):
return result.replace("4111-1111", "[card]") if isinstance(result, str) else None
def after_model(self, ctx):
if "secret" in (ctx.messages[-1]["content"][0].get("text", "") if ctx.messages[-1]["content"] else ""):
return HookAction(message=dict(ctx.messages[-1], content=[{"type": "text", "text": "[redacted]"}]))
return None
agent = Agent(
FakeModel([FakeReply.tool_calls([("lookup", {"order_id": "a-1"}), ("weather", {"city": "Paris"})]), "The secret answer"]),
tools=[lookup, weather],
hooks=[Guard()],
)
result = await agent.run("where is it?")
show("argument the tool received", seen)
show("what the model saw from the tools", [str(m["content"]) for m in tool_results(result)])
show("final answer", result.text)
check("before_tool changed the argument", seen == ["A-1"])
check("after_tool redacted the card number", "[card]" in str(tool_results(result)[0]["content"]))
check("a hook skipped a tool with a cached value", "sunny (cached)" in str(tool_results(result)[1]["content"]))
check("after_model replaced the reply", result.text == "[redacted]")
class Budget:
def before_model(self, ctx):
return HookAction(end=True, output="Stopped by the budget hook.") if ctx.model_calls >= 1 else None
@tool
def noop() -> str:
"""Does nothing."""
return "ok"
result = await Agent(FakeModel([FakeReply.tool_call("noop", {}), "never"], by_turn=True), tools=[noop], hooks=[Budget()]).run("go")
show("a hook ended the run", result.output)
check("the budget hook stopped it", result.output == "Stopped by the budget hook.")✓Calling a model directly: chat, stream, JSON output, embed
The Gateway class gives retries, fallbacks and schema-checked output without an agent.
Checks
- the reply says which model answered
- chunks add up to the reply
- it fails with a stable code
What it printed
- primary fails, the backup answers
{ "text": "from the backup", "model": "fake:backup" }- stream chunks
[ "Hello there." ]
- JSON output, checked against the schema
{ "city": "Oslo", "temp": 3 }- a structure that does not match
output_invalid
The code that ran
async def gateway_sdk():
gw = Gateway(fallbacks=[FakeModel(["from the backup"], name="backup")], retry=Retry(initial_backoff=0.01, jitter=False))
reply = await gw.chat(FakeModel([], fail_first=10, name="primary"), "hi")
show("primary fails, the backup answers", {"text": reply.text, "model": reply.model})
check("the reply says which model answered", reply.model == "fake:backup")
chunks = [c async for c in Gateway().stream(FakeModel(["Hello there."]), "hi")]
show("stream chunks", [c.text for c in chunks])
check("chunks add up to the reply", "".join(c.text for c in chunks) == "Hello there.")
weather = {"type": "object", "properties": {"city": {"type": "string"}, "temp": {"type": "integer"}}, "required": ["city", "temp"]}
reply = await Gateway().chat(FakeModel(['{"city": "Oslo", "temp": 3}']), "Weather?", output=weather)
show("JSON output, checked against the schema", reply.output)
try:
await Gateway().chat(FakeModel(['{"city": "Oslo"}']), "Weather?", output=weather)
check("a wrong structure is refused", False)
except criogen.CriogenError as error:
show("a structure that does not match", error.code)
check("it fails with a stable code", error.code == "output_invalid")Observability
✓Tracing to OpenTelemetry and Langfuse, without content
Every run is exported as a trace to any OTLP collector; prompts and answers stay out unless you ask.
Checks
- a trace reached the collector
- the prompt text was left out
What it printed
- what the collector received
2870 bytes, 3 spans: ['chat fake', 'model', 'criogen.run']
The code that ran
async def telemetry():
received = []
class Collector(http.server.BaseHTTPRequestHandler):
def do_POST(self):
received.append(self.rfile.read(int(self.headers["Content-Length"])).decode())
self.send_response(200)
self.end_headers()
def log_message(self, *args):
pass
server = http.server.HTTPServer(("127.0.0.1", 0), Collector)
threading.Thread(target=server.serve_forever, daemon=True).start()
try:
tel = Telemetry.otlp(f"http://127.0.0.1:{server.server_port}/v1/traces", service_name="feature-tour")
agent = Agent(FakeModel(["fine"]), telemetry=tel)
await agent.run("a private question")
await tel.flush()
body = "".join(received)
spans = json.loads(received[0])["resourceSpans"][0]["scopeSpans"][0]["spans"] if received else []
show("what the collector received", f"{len(body)} bytes, {len(spans)} spans: {[s['name'] for s in spans]}")
check("a trace reached the collector", "resourceSpans" in body)
check("the prompt text was left out", "a private question" not in body)
await tel.shutdown()
finally:
server.shutdown()Patterns and specialists
✓Sequential and parallel pipelines in one line
Registered specialists chain with '>' or run side by side with '|'; steps are traced and parallel branches overlap.
Checks
- both specialists ran, in order
- they overlapped (about 0.4 s, not 1.6 s)
What it printed
- researcher > writer
ended: done 1. researcher: a step's answer 2. writer: a step's answer
- four 0.4 s analysts at the same time
0.42 s, then: All four agree.
The code that ran
async def patterns_pipelines():
from criogen import patterns
model = FakeModel(["a step's answer"])
flow = patterns.quick("researcher > writer", model=model)
result = await flow.run("solar panels")
show("researcher > writer", result.explain())
check("both specialists ran, in order", [t["step"] for t in result.trace] == ["researcher", "writer"])
slow = {f"analyst{i}": Agent(FakeModel(["done"], delay=0.4)) for i in range(4)}
started = time.perf_counter()
result = await patterns.parallel(slow, synthesizer=("summary", Agent(FakeModel(["All four agree."])))).run("review this")
took = time.perf_counter() - started
show("four 0.4 s analysts at the same time", f"{took:.2f} s, then: {result.output}")
check("they overlapped (about 0.4 s, not 1.6 s)", took < 1.2)✓Review and critique, with a loop that always ends
A critic sends a draft back until it approves; a cap stops any loop that never converges.
Checks
- it ended on approval with the third draft
- the cap ended it and the last draft was kept
- no loop runs more than the ceiling
What it printed
- three rounds
ended: approved 1. step1 (draft 1): draft one 2. step2: REVISE: add a number 3. step1 (draft 2): draft two 4. step2: REVISE: shorter 5. step1 (draft 3): draft three 6. step2: APPROVED
- a critic that never approves, max_rounds=2
max_rounds, kept the last draft: d2
- max_iterations=10000 is clamped to
50
The code that ran
async def patterns_review_loop():
from criogen import patterns
writer = Agent(FakeModel(["draft one", "draft two", "draft three"]))
critic = Agent(FakeModel(["REVISE: add a number", "REVISE: shorter", "APPROVED"]))
result = await patterns.review_and_critique(writer, critic, max_rounds=5).run("Write a blurb")
show("three rounds", result.explain())
check("it ended on approval with the third draft", result.stop_reason == "approved" and result.output == "draft three")
stubborn = await patterns.review_and_critique(
Agent(FakeModel(["d1", "d2"])), Agent(FakeModel(["REVISE: a", "REVISE: b"])), max_rounds=2
).run("Write a blurb")
show("a critic that never approves, max_rounds=2", f"{stubborn.stop_reason}, kept the last draft: {stubborn.output}")
check("the cap ended it and the last draft was kept", stubborn.stop_reason == "max_rounds" and stubborn.output == "d2")
asked = await patterns.loop([lambda t: t + "!"], until=lambda t, n: False, max_iterations=10_000).run("x")
show("max_iterations=10000 is clamped to", len(asked.trace))
check("no loop runs more than the ceiling", len(asked.trace) == patterns.CEILING)✓Coordinator, hierarchy and swarm
A coordinator picks a specialist, specialists can be nested, and a swarm hands work over without a coordinator.
Checks
- the coordinator routed and answered
- agents handed over until FINAL
- the hand-off cap ended it
What it printed
- coordinator called the 'orders' specialist
[ [ { "text": "Order A-1 shipped.", "type": "text" } ] ]- swarm hand-offs
ended: final 1. market: Demand is high. HANDOFF: engineering 2. engineering: Feasible in 3 months. HANDOFF: finance 3. finance: Budget fits. FINAL
- two agents that keep passing it back
max_handoffs
The code that ran
async def patterns_routing():
from criogen import patterns
orders = Agent(FakeModel(["Order A-1 shipped."]))
triage = patterns.coordinator(
FakeModel([FakeReply.tool_call("orders", {"input": "where is A-1"}), "It shipped."]),
{"orders": (orders, "order status questions")},
)
result = await triage.run("where is A-1")
show("coordinator called the 'orders' specialist", [m["content"] for m in tool_results(result)])
check("the coordinator routed and answered", result.text == "It shipped.")
team = {
"market": Agent(FakeModel(["Demand is high.\nHANDOFF: engineering"])),
"engineering": Agent(FakeModel(["Feasible in 3 months.\nHANDOFF: finance"])),
"finance": Agent(FakeModel(["Budget fits.\nFINAL"])),
}
result = await patterns.swarm(team, start="market").run("Should we build it?")
show("swarm hand-offs", result.explain())
check("agents handed over until FINAL", result.stop_reason == "final" and len(result.trace) == 3)
ping = {"a": Agent(FakeModel(["x\nHANDOFF: b"] * 9)), "b": Agent(FakeModel(["y\nHANDOFF: a"] * 9))}
result = await patterns.swarm(ping, max_handoffs=3).run("go")
show("two agents that keep passing it back", result.stop_reason)
check("the hand-off cap ended it", result.stop_reason == "max_handoffs")✓Human in the loop as a pattern, durable
A step's draft waits for a person; another process approves it later, and the audit trail names both.
Checks
- nothing is released yet
- the edit is the output
- the audit trail names both people
What it printed
- it waits with the draft
{ "draft": "Release 120 rows.", "question": "Approve, edit or reject this draft." }- the reviewer edited it
Release 100 rows.
The code that ran
async def patterns_human_review():
from criogen import patterns
storage = Storage(audit=True)
first = patterns.human_review(Agent(FakeModel(["Release 120 rows."]))).compile(storage).thread("release-1", actor="user:ada")
turn = await first.run({"input": "Release the dataset?"})
show("it waits with the draft", (await first.interrupts())[0].payload)
check("nothing is released yet", turn.suspended)
other = patterns.human_review(Agent(FakeModel(["unused"]))).compile(storage).thread("release-1", actor="user:boss")
done = await other.resume({"review": {"decision": "edit", "text": "Release 100 rows."}})
show("the reviewer edited it", done.state["output"])
check("the edit is the output", done.state["output"] == "Release 100 rows.")
records = await storage.audit_records(thread_id="release-1")
check("the audit trail names both people", {"user:ada", "user:boss"} <= {r.get("actor") for r in records})✓Specialist agents you can extend
Default recipes for common jobs; extend one, register it, and use it by name anywhere.
Checks
- the original recipe is unchanged
- a registered recipe works as a step
What it printed
- built in
[ "classifier", "code_reviewer", "critic", "extractor", "fact_checker", "planner", "researcher", "summarizer", "support_agent", "translator", "writer" ]
- extended instructions end with
Always give two sources.
- used by name
[ "careful-researcher", "summarizer" ]
The code that ran
async def specialists_demo():
from criogen import patterns, specialists
show("built in", specialists.available())
careful = specialists.researcher.extend("careful-researcher", add="Always give two sources.")
show("extended instructions end with", careful.instructions.splitlines()[-1])
check("the original recipe is unchanged", "two sources" not in specialists.researcher.instructions)
specialists.register(careful, replace=True)
try:
flow = patterns.sequential(["careful-researcher", "summarizer"], model=FakeModel(["ok"]))
result = await flow.run("topic")
show("used by name", [t["step"] for t in result.trace])
check("a registered recipe works as a step", [t["step"] for t in result.trace] == ["careful-researcher", "summarizer"])
finally:
specialists.unregister("careful-researcher")✓Web search tools (Tavily, Brave) and page reading
web.search() works from an environment key; results are bounded, the key never reaches the model, and private addresses are refused.
Checks
- 20 results were cut to 3, with short snippets
- the key went in a header and nowhere the model can see
- private network addresses are refused
What it printed
- what the model saw
{ "answer": "Solar is growing.", "results": 3, "first": "Result 0" }- reading a private address
[{'text': 'Error: that address is on a private network; only public web pages can be read', 'type': 'text'}]- with TAVILY_API_KEY or BRAVE_API_KEY set
web.search() needs no arguments; the researcher specialist picks it up automatically
The code that ran
async def web_search_demo():
from criogen import web
seen = []
class Fake(http.server.BaseHTTPRequestHandler):
def do_POST(self):
payload = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
seen.append((self.headers.get("authorization"), payload))
body = json.dumps({"answer": "Solar is growing.", "results": [
{"title": f"Result {i}", "url": f"https://example.com/{i}", "content": "x" * 2000} for i in range(20)]}).encode()
self.send_response(200)
self.send_header("content-length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
server = http.server.HTTPServer(("127.0.0.1", 0), Fake)
threading.Thread(target=server.serve_forever, daemon=True).start()
try:
search = web.tavily_search("tvly-demo-key", max_results=3, base_url=f"http://127.0.0.1:{server.server_port}")
agent = Agent(FakeModel([FakeReply.tool_call("web_search", {"query": "solar panels"}), "Summary."]), tools=[search])
result = await agent.run("What is new in solar?")
value = tool_results(result)[0]["content"][0]["value"]
show("what the model saw", {"answer": value["answer"], "results": len(value["results"]), "first": value["results"][0]["title"]})
check("20 results were cut to 3, with short snippets", len(value["results"]) == 3 and all(len(r["snippet"]) <= 600 for r in value["results"]))
check("the key went in a header and nowhere the model can see", seen[0][0] == "Bearer tvly-demo-key" and "tvly-demo-key" not in json.dumps(result.messages))
finally:
server.shutdown()
refused = Agent(FakeModel([FakeReply.tool_call("read_page", {"url": "http://127.0.0.1:1/admin"}), "I could not read it."]), tools=[web.fetch()])
result = await refused.run("Read http://127.0.0.1:1/admin")
show("reading a private address", str(tool_results(result)[0]["content"]))
check("private network addresses are refused", "private network" in str(tool_results(result)[0]["content"]))
show("with TAVILY_API_KEY or BRAVE_API_KEY set", "web.search() needs no arguments; the researcher specialist picks it up automatically")12 of 12 scenarios passed. A real model (Gemini gemini-2.5-flash) made the decisions, so these check behaviour, never exact wording; a scenario that needed a second attempt says so. criogen 0.1.0 (Python), Windows 11, 2026-10-24.
With a real model
✓A real model chooses and calls a tool
Given a question, the model decides to call get_order with the right argument and answers from the result.
Checks
- it looked the order up with the right id
- the answer reflects the tool's result
What it printed
- the model called get_order with
[ "A-1001" ]
- answer
Your order A-1001 was shipped and should arrive tomorrow.
- usage
{ "cache_read_tokens": 0, "cache_write_tokens": 0, "input_tokens": 214, "output_tokens": 37, "reasoning_tokens": 0 }
The code that ran
async def live_tools():
calls = []
@tool(read_only=True)
def get_order(order_id: str) -> dict:
"""Look up an order by its id."""
calls.append(order_id)
return {"order_id": order_id, "status": "shipped", "eta": "tomorrow"}
result = await Agent(real(), instructions=SUPPORT, tools=[get_order]).run("Where is order A-1001?")
show("the model called get_order with", calls)
show("answer", result.text)
show("usage", result.usage)
check("it looked the order up with the right id", calls == ["A-1001"])
check("the answer reflects the tool's result", "ship" in (result.text or "").lower())✓A real model asks to refund; a person decides
The model proposes a destructive call; the run pauses before it, and it runs exactly once after approval.
Checks
- the run paused before the refund
- the refund ran exactly once
- a rejected call never ran
What it printed
- the model asked for
[ { "tool": "issue_refund", "args": { "amount": 24, "order_id": "A-1001" } } ]- after approval the model said
OK. I have refunded 24 dollars from order A-1001.
- after rejection the model said
I am sorry, but I am unable to refund this order. My apologies.
The code that ran
async def live_approval():
refunds = []
@tool(destructive=True)
def issue_refund(order_id: str, amount: float) -> str:
"""Refund an order. Amount in dollars."""
refunds.append((order_id, amount))
return "refunded"
storage = Storage()
def make():
return Agent(real(), instructions=SUPPORT, tools=[issue_refund], approve=["issue_refund"], storage=storage)
chat = make().thread("t-1", actor="user:ada")
reply = await chat.send("Order A-1001 arrived broken. Refund it, 24 dollars.")
show("the model asked for", [{"tool": r.tool, "args": r.args} for r in reply.approvals])
check("the run paused before the refund", reply.needs_approval and refunds == [])
reply = await make().thread("t-1", actor="user:boss").approve() # another process, in real life
show("after approval the model said", reply.text)
check("the refund ran exactly once", len(refunds) == 1 and refunds[0][0] == "A-1001")
chat = make().thread("t-2")
await chat.send("Order A-2002 arrived broken. Refund it, 10 dollars.")
reply = await chat.reject("Our policy does not allow refunds on this order.")
show("after rejection the model said", reply.text)
check("a rejected call never ran", len(refunds) == 1)✓A real model's answer as a checked structure
The answer comes back as data that matches your schema, not as text to parse.
Checks
- it is a valid structure
- it chose an allowed category
What it printed
- output
{ "category": "billing", "urgent": true }
The code that ran
async def live_structured():
schema = {
"type": "object",
"properties": {"category": {"enum": ["billing", "shipping", "other"]}, "urgent": {"type": "boolean"}},
"required": ["category", "urgent"],
"additionalProperties": False,
}
result = await Agent(real(), instructions="Classify the customer's message.", output=schema).run(
"I was charged twice for order A-1001 and need it fixed today!"
)
show("output", result.output)
check("it is a valid structure", set(result.output) == {"category", "urgent"})
check("it chose an allowed category", result.output["category"] in {"billing", "shipping", "other"})✓Streaming from a real model
Text arrives in several events as the model writes, and the saved reply equals what was streamed.
Checks
- the text came in more than one piece
- the pieces add up to the saved reply
What it printed
- number of text events
2
- first events
[ "A database index drastically speeds up data retrieval. Like a book's index, it helps the database quickly", " find specific rows without scanning the entire table. It works by creating a sorted reference to column values and their locations, significantly optimizing query performance for searches." ]
The code that ran
async def live_streaming():
stream = Agent(real(), instructions="Answer in about 40 words.").thread("s").stream("Explain what a database index is.")
texts = []
async for event in stream:
if event.type == "text":
texts.append(event.text)
reply = await stream.reply()
show("number of text events", len(texts))
show("first events", texts[:3])
check("the text came in more than one piece", len(texts) > 1)
check("the pieces add up to the saved reply", "".join(texts) == reply.text)✓A real model remembers across conversations
A fact stated in one conversation is stored, and recalled in a different one for the same user.
Checks
- the model saved a fact
- it took the remembered fact into account
- another user's memory is empty
What it printed
- stored for users/ada
[ [ "diet", "vegetarian" ] ]- in a new conversation the model said
Your diet is vegetarian.
The code that ran
async def live_memory():
storage = Storage()
agent = Agent(
real(),
instructions="You are a helpful assistant. When the user tells you a lasting fact about themselves, save it with the remember tool.",
storage=storage,
memory=Memory.by_state("user_id", "users"),
)
await agent.thread("monday").send("Please remember that I am vegetarian.", user_id="ada")
facts = await storage.search("users/ada")
show("stored for users/ada", [(f["key"], f["value"]) for f in facts])
check("the model saved a fact", len(facts) > 0)
reply = await agent.thread("friday").send("What do you know about my diet? One short sentence.", user_id="ada")
show("in a new conversation the model said", reply.text)
check("it took the remembered fact into account", any(w in (reply.text or "").lower() for w in ("vegetarian", "veggie", "meat-free", "vegan", "plant")))
check("another user's memory is empty", await storage.search("users/bob") == [])✓Hooks keep secrets away from a real model
An after_tool hook redacts a card number before the model ever sees it.
Checks
- the card number never reached the model
- the model still answered
What it printed
- what the model saw from the tool
[ "[{'text': 'order A-1001: paid with card [card]', 'type': 'text'}]" ]- answer
Order A-1001 was paid with card.
The code that ran
async def live_hooks():
@tool(read_only=True)
def get_payment(order_id: str) -> str:
"""Payment details for an order."""
return f"order {order_id}: paid with card 4111-1111-1111-1111"
class Redact:
def after_tool(self, call, result):
return result.replace("4111-1111-1111-1111", "[card]") if isinstance(result, str) else None
result = await Agent(real(), instructions=SUPPORT, tools=[get_payment], hooks=[Redact()]).run("How was order A-1001 paid?")
seen = json.dumps(result.messages)
show("what the model saw from the tool", [str(m["content"]) for m in tool_results(result)])
show("answer", result.text)
check("the card number never reached the model", "4111" not in seen)
check("the model still answered", bool(result.text))✓An audit trail of a real run, without its content
Model calls, tool calls and the approver are recorded and verifiable; the conversation text is not.
Checks
- the chain verifies
- the model call was recorded
- no message content is in the records
What it printed
- records (action, actor)
[ [ "model.called", "user:ada" ], [ "run.finished", "user:ada" ], [ "approval.requested", "user:ada" ], [ "approval.decided", "user:dr-rao" ], [ "tool.executed", "user:dr-rao" ], [ "model.called", "user:dr-rao" ], [ "run.finished", "user:dr-rao" ] ]- hash chain verification
{ "first_broken": null, "head": [ 7, "44a990dbafe31b4f1612e6fab7deb8b2372b5e75fbe9b15c750544affc558fe4" ], "ok": true, "records": 7 }
The code that ran
async def live_audit():
storage = Storage(audit=True)
@tool(destructive=True)
def issue_refund(order_id: str, amount: float) -> str:
"""Refund an order."""
return "refunded"
agent = Agent(real(), instructions=SUPPORT, tools=[issue_refund], approve=["issue_refund"], storage=storage)
reply = await agent.thread("visit-9", actor="user:ada").send("My name is Ada Lovelace. Refund order A-1001, 5 dollars.")
if reply.needs_approval:
await agent.thread("visit-9", actor="user:dr-rao").approve()
records = await storage.audit_records(thread_id="visit-9")
show("records (action, actor)", [(r["action"], r.get("actor")) for r in records])
report = await storage.verify_audit()
show("hash chain verification", report)
check("the chain verifies", report["ok"] is True)
check("the model call was recorded", "model.called" in [r["action"] for r in records])
check("no message content is in the records", "Lovelace" not in json.dumps(records))✓A broken model falls back to a working one
When the primary model fails, the agent answers from the fallback.
Checks
- the fallback model answered
What it printed
- answer
ready
The code that ran
async def live_fallback():
agent = Agent(
LIVE["broken"](),
fallbacks=[real()],
model_retry=Retry(max_attempts=1, initial_backoff=0.01, jitter=False),
fallback_on=["not_found"], # by default only rate limits and outages fall back; a wrong model name is opt-in
)
result = await agent.run("Reply with the single word: ready")
show("answer", result.text)
check("the fallback model answered", bool(result.text))✓Direct model calls: chat, JSON, stream, embeddings
The Gateway class calls a real model without an agent, with schema-checked output and embeddings.
Checks
- it answered
- the output matches the schema
- it streamed in pieces
- one vector per text
What it printed
- chat
{ "text": "Paris", "model": "gemini:gemini-2.5-flash" }- JSON output
{ "urgent": true }- stream pieces
4
- embeddings
2 vectors of 3072 numbers
The code that ran
async def live_gateway():
gw = Gateway()
reply = await gw.chat(real(), "What is the capital of France?", instructions="Answer in one word.")
show("chat", {"text": reply.text, "model": reply.model})
check("it answered", "paris" in (reply.text or "").lower())
triage = {"type": "object", "properties": {"urgent": {"type": "boolean"}}, "required": ["urgent"]}
reply = await gw.chat(real(), "Patient reports crushing chest pain.", output=triage)
show("JSON output", reply.output)
check("the output matches the schema", isinstance(reply.output.get("urgent"), bool))
chunks = [c.text async for c in gw.stream(real(), "Write about 120 words on why rain matters to farmers.")]
show("stream pieces", len(chunks))
check("it streamed in pieces", len(chunks) > 1)
if LIVE["embedder"] is None:
show("embeddings", "skipped: this provider has no embedding model")
return
vectors = await gw.embed(LIVE["embedder"](), ["first note", "second note"])
show("embeddings", f"{len(vectors)} vectors of {len(vectors[0])} numbers")
check("one vector per text", len(vectors) == 2 and len(vectors[0]) > 10)✓A real model as a workflow step
A graph mixes a plain function and a real agent; the agent's reply lands in the shared state.
Checks
- the agent added its reply to the state
What it printed
- roles
[ "user", "assistant" ]
- the agent's reply
A refund is money returned to a customer for a purchase they made, often because the item was returned or the service was unsatisfactory.
The code that ran
async def live_workflow():
wf = Workflow(reducers={"messages": "upsert_by_id"})
question = {"id": "m1", "role": "user", "content": [{"type": "text", "text": "In one sentence: what is a refund?"}]}
wf.add_node("ask", lambda s: {"messages": [question]})
wf.add_node("agent", Agent(real(), instructions="Be brief."))
wf.start("ask").edge("ask", "agent").edge("agent", END)
result = await wf.compile().run({})
roles = [m["role"] for m in result.state["messages"]]
show("roles", roles)
show("the agent's reply", result.state["messages"][-1]["content"][0]["text"])
check("the agent added its reply to the state", roles == ["user", "assistant"])✓Specialists and patterns with a real model
One-line pipelines of built-in specialists work with a real model: a writer and critic loop, and a planner into a summarizer.
Checks
- it ended (approved, or the round cap) with an output
- the blurb has the required facts
- both steps ran
What it printed
- writer <-> critic
ended: approved 1. writer (draft 1): Elevate your workspace and well-being with our innovative standing desk, designed for optimal comfort and productivity throughout your day. … 2. critic: APPROVED
- planner > summarizer
ended: done 1. planner: 1. Decide on 2-3 baked goods to make. 2. Create a shopping list for all ingredients needed. 3. Buy all ingredients from the grocery store. 4… 2. summarizer: Decide on 2-3 baked goods, create a shopping list, and buy all ingredients. Bake goods on Friday. Set up your sale table and display Saturda…
The code that ran
async def live_patterns():
from criogen import patterns
review = await patterns.quick("writer <-> critic", model=real()).run(
"Write a two-sentence product blurb for a standing desk. It must mention the price $249 and free delivery."
)
show("writer <-> critic", review.explain())
check("it ended (approved, or the round cap) with an output", review.stop_reason in ("approved", "max_rounds") and bool(review.output))
check("the blurb has the required facts", "$249" in review.output and "deliver" in review.output.lower())
plan = await patterns.quick("planner > summarizer", model=real()).run("Goal: hold a small bake sale on Saturday")
show("planner > summarizer", plan.explain())
check("both steps ran", [t["step"] for t in plan.trace] == ["planner", "summarizer"])✓A real coordinator picks the right specialist
Given two specialists, the coordinator model chooses the one that fits and answers from it.
Checks
- the order specialist's tool ran with the right id
What it printed
- answer
Order A-1001 has been shipped.
- specialists the coordinator called
[ "orders" ]
The code that ran
async def live_coordinator():
from criogen import patterns
asked = []
@tool(read_only=True)
def get_order(order_id: str) -> dict:
"""Look up an order by id."""
asked.append(order_id)
return {"order_id": order_id, "status": "shipped"}
orders = Agent(real(), instructions="You answer order status questions. Use get_order. Be brief.", tools=[get_order])
jokes = Agent(real(), instructions="You tell one short, clean joke.")
triage = patterns.coordinator(real(), {"orders": (orders, "Order status and delivery questions"), "jokes": (jokes, "Tells a joke")})
result = await triage.run("Where is order A-1001?")
show("answer", result.text)
show("specialists the coordinator called", [m.get("name") or m.get("tool") for m in tool_results(result)])
check("the order specialist's tool ran with the right id", asked == ["A-1001"])Feature list and what shows it
| Feature (from the status page) | Scripted model | Real model |
|---|---|---|
| Agents with tools, limits, structured output | ✓ agents✓ validation✓ tool-errors | ✓ live-tools✓ live-structured |
| Human approval (approve, reject, edit) | ✓ approvals | ✓ live-approval |
| Threads: saved conversations, retry, history, rewind | ✓ threads✓ crash-recovery | – |
| Streaming and cancellation | ✓ streaming | ✓ live-streaming |
| Storage: SQLite, in memory (Postgres: not in this tour) | ✓ storage | – |
| Encryption at rest, audit trail, retention | ✓ encryption✓ audit✓ retention | ✓ live-audit |
| Long-term memory, by keyword and by meaning | ✓ memory✓ semantic-memory | ✓ live-memory |
| Workflows, durable pauses for a person | ✓ workflows✓ durable-workflows | ✓ live-workflow |
| Hooks around model and tool calls | ✓ hooks | ✓ live-hooks |
| Agents as tools and as workflow steps | ✓ agents-compose✓ agent-nodes | ✓ live-workflow |
| Fallback models, retry policies | ✓ resilience | ✓ live-fallback |
| Tracing to OpenTelemetry (Langfuse uses the same export) | ✓ telemetry | – |
| Calling a model directly (Gateway) | ✓ gateway-sdk | ✓ live-gateway |
| Patterns: pipelines, review loops, coordinator, swarm, human review (demo runs the Python form) | ✓ patterns-pipelines✓ patterns-review-loop✓ patterns-routing✓ patterns-human-review | ✓ live-patterns✓ live-coordinator |
| Specialist agents you can extend (demo runs the Python form) | ✓ specialists | ✓ live-patterns |
| Web search and page reading (demo runs the Python form; Tavily needs a key, so a local stand-in server is used) | ✓ web-search | – |
Not shown in this tour
- Postgres storage and cross-process locks: their tests need a database server and run in CI.
- TypeScript-only Zod / Standard Schema tool input, and the Rust API: the same engine; see the docs for each language.
- The LLM gateway server (OpenAI- and Anthropic-compatible endpoints, rate limits, budgets): see the gateway guide.
- Langfuse itself: the tour exports to a local OpenTelemetry collector, which Langfuse accepts.
Run it yourself
The tour is one Python file. It writes the files this page reads; keys for the real-model tour are read from ~/.criogen-keys.env and never written anywhere.
pip install criogenai # the Python package
python demos/feature-tour/tour.py # simulated model: no network, no keys
python demos/feature-tour/tour.py --live gemini # a real model (GEMINI_API_KEY); or: --live openai