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#

PythonTypeScriptWhat 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({...})sameAnswer 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:

FieldMeaning
idThe checkpoint id, for rewind
versionGrows by one with every write to the thread
statusin_progress, suspended, completed or failed
sourceWhat wrote it: input, step, suspend, failure, fork
stepThe engine step it was taken after
run_id, parent_id, created_at_msThe 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#

CodeWhenWhat to do
thread_busyAnother run holds the thread (after waiting, or at once with on_busy="reject")Try again later, or tell the user to wait
thread_suspendedAn approval waitsapprove, reject or decide first
thread_interruptedThe last run never finished (the process died)retry()
resume_invalidretry() with nothing to retry, or a decision that does not fitCheck history()
checkpoint_not_foundrewind to an id the thread does not haveUse an id from history()
cancelledThe call was cancelled (signal, task cancel, a left stream)retry() to continue it

Next#