Conversations (threads)
A thread is one conversation with an agent, kept in the agent's
storage. Use a thread whenever a person comes back:
a chat, a support ticket, a patient case. Each send runs one turn on top of
everything said before, and progress is saved as it happens: when a message
arrives, after every step, and after every tool call. What a checkpoint and a
thread status are is explained in
State, storage and threads.
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?;In Python, send also has a blocking form for scripts and notebooks:
# Scripts and notebooks: the same calls without asyncio.
reply = agent.thread("user-42").send_sync("Hello")
result = agent.run_sync("Where is order A-1001?")Operations#
| Python | TypeScript | What it does |
|---|---|---|
agent.thread(id, actor=None) | agent.thread(id, { actor }) | A handle; the thread is created by its first send |
send(text, **state) | send(text, { state, signal }) | Runs one turn; returns a Reply |
stream(text, **state) | stream(text, { state }) | The same, yielding events (Streaming) |
approve(), reject(reason), decide({...}) | same | Answer a waiting approval (Human approval) |
retry() | retry({ signal }) | Continue a failed or interrupted run |
messages() | messages() | The stored conversation |
history(limit=20) | history(limit) | Checkpoints, newest first |
rewind(checkpoint_id) | rewind(checkpointId) | Make an older checkpoint the head again |
delete() | delete() | Delete the thread and its checkpoints; True if it existed |
A handle holds nothing but the id and the actor: make one per request, in any process. Two handles on the same id are the same conversation.
What a thread guarantees#
It remembers. The model sees the whole stored conversation on every turn. Long conversations cost more tokens each turn; start a new thread for a new topic, or keep lasting facts in memory instead.
One run at a time. A second call on a busy thread waits for the first by
default (up to 60 s), then fails with thread_busy. Storage can instead
reject at once, or interrupt the running call; see
Storage. With Postgres this
holds across processes and machines.
Nothing lost on a crash. If the process dies mid-run, the thread keeps
the unfinished run. The next send fails with thread_interrupted until
retry() continues the run from where it stopped. Tool calls that had
finished are not run again.
Failures can be continued. When a call fails (the model was down, a
limit was reached, the call was cancelled), the failed run is saved.
retry() continues it from the failed step; send() leaves it and starts a
new turn.
Approvals wait. A run that needs approval is saved as suspended; send
fails with thread_suspended until someone answers.
Handling the states in your code#
from criogenai import CriogenError, ThreadSuspendedError
chat = agent.thread("user-42")
try:
reply = await chat.send("Book my follow-up visit")
except ThreadSuspendedError:
reply = await chat.reject("Cancelled by the user") # or approve() / decide(...)
except CriogenError as error:
if error.code != "thread_interrupted": # a crash left the last run unfinished
raise
reply = await chat.retry() # finish that run first; finished tool calls are not run again
# The stored conversation (the same message format as result.messages).
for message in await chat.messages():
print(message["role"], message["content"][0].get("text", ""))import { CriogenError, ThreadSuspendedError } from "criogenai";
const thread = agent.thread("user-42");
let answer;
try {
answer = await thread.send("Book my follow-up visit");
} catch (error) {
if (error instanceof ThreadSuspendedError) {
answer = await thread.reject("Cancelled by the user"); // or approve() / decide(...)
} else if (error instanceof CriogenError && error.code === "thread_interrupted") {
answer = await thread.retry(); // a crash left the last run unfinished; finished tool calls are not run again
} else {
throw error;
}
}
// The stored conversation (the same message format as result.messages).
for (const message of await thread.messages()) {
const content = message.content as { text?: string }[];
console.log(message.role, content[0]?.text ?? "");
}History and rewind#
history(n) lists the newest checkpoints first. Each is a dict / object:
| Field | Meaning |
|---|---|
id | The checkpoint id, for rewind |
version | Grows by one with every write to the thread |
status | in_progress, suspended, completed or failed |
source | What wrote it: input, step, suspend, failure, fork |
step | The engine step it was taken after |
run_id, parent_id, created_at_ms | The run, the checkpoint before it, and when (ms since 1970) |
rewind(id) writes a new head that copies the older checkpoint
(source: "fork"), so the next send or retry continues from there. The newer
checkpoints are kept in storage, off the history: nothing is overwritten.
Use it to undo a turn, or to try again from before a bad answer.
Deleting#
delete() removes the thread and every checkpoint of it. It does not remove
long-term memory: that belongs to the person, not the conversation. To erase
everything about a person, delete their threads and call
forget_all("users/<id>") (forgetAll) too. For deleting old threads on a
schedule, see retention.
Thread ids#
Choose ids your app can find again: a ticket number, a chat id, a UUID you store with the user. Thread ids are stored in clear text even in an encrypted database, so use opaque ids (a UUID), not names or record numbers.
Errors#
| Code | When | What to do |
|---|---|---|
thread_busy | Another run holds the thread (after waiting, or at once with on_busy="reject") | Try again later, or tell the user to wait |
thread_suspended | An approval waits | approve, reject or decide first |
thread_interrupted | The last run never finished (the process died) | retry() |
resume_invalid | retry() with nothing to retry, or a decision that does not fit | Check history() |
checkpoint_not_found | rewind to an id the thread does not have | Use an id from history() |
cancelled | The call was cancelled (signal, task cancel, a left stream) | retry() to continue it |
Next#
- Streaming: show a turn as it happens.
- Storage: SQLite, Postgres and busy threads.
- Workflows: threads for workflows.
- API: Python
Thread, TypeScriptThread.