TypeScript API: Agents
The agent: a model, tools and instructions; runs, threads, streams and approvals. Generated from the declarations and doc comments of criogenai 0.1.7. See also: TypeScript API overview, the same module in Python.
import {
Agent,
AgentOptions,
Thread,
Stream,
Reply,
RunResult,
RunOptions,
CallOptions,
Decision,
ApprovalRequest,
Retry,
AgentTool,
StreamEvent,
} from "criogenai";Contents#
| Name | Kind | Summary |
|---|---|---|
Agent | class | A model with tools, instructions and (optionally) approval, typed output, storage and memory. |
AgentOptions | interface | Everything an Agent is built from. |
Thread | class | One conversation, kept in the agent's storage. |
Stream | class | A streamed thread call: for await (const event of stream), then await stream.reply(). |
Reply | interface | A thread call's result, saved as the thread's new head. |
RunResult | interface | What a run produced; durationMs, answeredBy and models say which models answered and how long it took. |
RunOptions | interface | Options of agent.run and thread.send. |
CallOptions | interface | Options of every thread call that takes no input (approve, reject, decide, retry). |
Decision | type | A reviewer's answer to one call (see Thread.decide). |
ApprovalRequest | interface | A tool call waiting for a reviewer's decision. |
Retry | interface | How often and how patiently to retry; missing fields keep the defaults. |
AgentTool | class | An agent as a tool of another agent (agent.asTool): it takes { input } and returns the sub-agent's answer. |
StreamEvent | type | One streamed event. |
Agent#
class Agent<Output = unknown> {
constructor(options: AgentOptions);
}A model with tools, instructions and (optionally) approval, typed output, storage and memory. The loop runs in Rust.
Each run calls the model, runs the tools it asks for (concurrently, in
call order), feeds the results back, and repeats until the model answers
without tools or a limit is reached. Use run for one-off tasks and
thread(id) for conversations that are saved between calls.
Output types result.output; it is not checked at compile time, so
pass the matching JSON Schema as the output option.
Parameters
options(AgentOptions): The model, instructions, tools and the rest (seeAgentOptions).
Raises
TypeError: An entry oftoolsis not a tool, or a model string names no known provider.ValidationError: A model string's provider has no API key in the environment (invalid_value).ValidationError: An option the engine does not accept (e.g. an unknownfallbackOnkind).
Example
import { Agent, FakeModel, FakeReply, tool } from "criogenai";
const add = tool<{ a: number; b: number }>({
name: "add",
description: "Add two numbers.",
input: { type: "object", properties: { a: { type: "number" }, b: { type: "number" } }, required: ["a", "b"] },
run: ({ a, b }) => a + b,
});
const agent = new Agent({
model: new FakeModel([FakeReply.toolCall("add", { a: 2, b: 3 }), "2 + 3 = 5"]), // or "openai:gpt-5-mini"
instructions: "Use the add tool for arithmetic.",
tools: [add],
});
const result = await agent.run("What is 2 + 3?");
console.log(result.text, result.toolCalls); // "2 + 3 = 5" 1Agent.storage#
readonly storage: StorageThe storage that keeps this agent's threads (in memory unless the storage option says otherwise).
Agent.run#
run(input: string, options?: RunOptions): Promise<RunResult<Output>>Runs once (no thread). Nothing is saved: a run that stops for approval
cannot be continued, so use thread(id).send(...) with approve.
Parameters
input(string): The user's message.options(RunOptions, default{}):state(run state, e.g. the key per-user memory reads) andsignal(cancels the run).
Returns: The answer, the transcript, token usage and call counts.
Raises
ModelError: The model failed after its retries and fallbacks.LimitExceededError: A limit inlimitswas reached.ToolExecutionError: A tool threw andonToolExceptionis "fail".CancelledError:signalwas aborted.
Agent.asTool#
asTool(name: string, description: string): AgentToolThis agent as a tool of another agent: the tool takes input (text),
runs this agent, and returns its answer. How deep agents may call agents
is bounded by the nesting limit (default 8).
Parameters
name(string): The tool name the calling model sees (letters, digits,_and-).description(string): What this agent does, so the calling model knows when to use it.
Example
import { Agent, FakeModel, FakeReply } from "criogenai";
const researcher = new Agent({ model: new FakeModel(["The order shipped on Monday."]), instructions: "Find facts." });
const lead = new Agent({
model: new FakeModel([FakeReply.toolCall("research", { input: "When did order A-1 ship?" }), "It shipped on Monday."]),
tools: [researcher.asTool("research", "Looks up facts about orders.")],
});
console.log((await lead.run("When did A-1 ship?")).text);Agent.thread#
thread(threadId: string, options?: { actor?: string; }): Thread<Output>The conversation threadId (created on first use); actor names who acts, for the audit trail.
Nothing is read or written until you call a method of the thread.
Parameters
threadId(string): Any string that names the conversation in this agent's storage.options({ actor?: string; }, default{}):actor: who makes the calls (a user or service id), recorded in audit records.
Example
import { Agent, FakeModel } from "criogenai";
const agent = new Agent({ model: new FakeModel(["Hi Sam.", "Your name is Sam."]) });
const thread = agent.thread("support-42");
await thread.send("My name is Sam.");
const reply = await thread.send("What is my name?"); // the model sees the earlier turn
console.log(reply.text, reply.version);AgentOptions#
interface AgentOptions {
model: Model;
instructions?: string;
tools?: (Tool<any> | AgentTool | McpServer | McpTool | CollectionTool)[];
approve?: string[];
output?: JsonSchema;
limits?: { modelCalls?: number; toolCalls?: number; tokens?: number; toolTimeoutSecs?: number };
onToolException?: "returnToModel" | "returnGeneric" | "fail";
storage?: Storage | string;
memory?: Memory;
fallbacks?: Model[];
fallbackOn?: string[];
modelRetry?: Retry;
toolRetry?: Retry;
maxRetryAfterSecs?: number;
telemetry?: { readonly native: { readonly handle: number } };
hooks?: Hook[];
redact?: Redactor;
}Everything an Agent is built from. Only model is required.
Fields
model(Model): The model: a "provider:model" string such as "openai:gpt-5-mini" (the key comes from the provider's environment variable), or a model object (openai(...),azure(...),FakeModel,FunctionModel, ...).instructions(string, optional): The system message the model sees on every call.tools((AgentTool | Tool<any> | McpServer | McpTool | CollectionTool)[], optional): Tools:tool({...}),agent.asTool(...), an MCP server (all its tools) or one of its tools.approve(string[], optional): Names of tools that need human approval before they run. A call to one stops the run (reply.needsApproval); use a thread to approve, reject or edit it.output(JsonSchema, optional): JSON Schema of the final answer (result.output).limits({ modelCalls?: number; toolCalls?: number; tokens?: number; toolTimeoutSecs?: number; }, optional): Bounds on one run:modelCalls(default 25; retries of one call do not count),toolCalls(default 200),tokens(input plus output tokens across the run, checked before each model call; no limit by default) andtoolTimeoutSecs(per tool call, default 60). Reaching a limit fails the run withLimitExceededError; a tool that times out returns an error to the model instead.onToolException("returnToModel" | "returnGeneric" | "fail", optional): When a tool throws an exception that is not a ToolError: "returnToModel" (default; the model sees its name and message, the run goes on), "returnGeneric" (the model only learns that the tool failed; the text stays in the result'sartifact) or "fail" (the run fails with ToolExecutionError, the exception as itscause).storage(string | Storage, optional): Where threads are kept: aStorage, or a URL ("sqlite://x.db", "postgres://…"). Without it, threads live in memory and end with the process.memory(Memory, optional): Long-term memory (Memory.byState(...)orMemory.shared(...)): facts recalled before each model call, and (unless the memory's options turn them off) tools for the model to remember, forget and search them. Kept in the agent's storage.fallbacks(Model[], optional): Models tried in order when the previous one fails with a transient error (after its retries).fallbackOn(string[], optional): Other error kinds that also move to the next model, e.g. "context_overflow".modelRetry(Retry, optional): Retries of transient model errors (default: 3 attempts, backoff from 0.5 s).toolRetry(Retry, optional): Retries of tools marked readOnly or idempotent (default: 3 attempts).maxRetryAfterSecs(number, optional): The longest Retry-After the agent waits, in seconds (default 60).telemetry({ readonly native: { readonly handle: number; }; }, optional): Exports every run of this agent (Telemetry.langfuse(),Telemetry.otlp(url)).hooks(Hook[], optional): Code around the loop (seeHook):onStart,beforeModel,afterModel,beforeTool,afterTool,onEnd.redact(Redactor, optional): Applied to every text before each model call (input, replies, tool results), ahead of all hooks: e.g. strip patient identifiers before anything reaches a provider. The redacted transcript is what is saved.
Thread#
class Thread<Output = unknown> {
constructor(agent: Agent<Output>, id: string, actor?: string);
}One conversation, kept in the agent's storage.
Every call loads the saved conversation, runs, and saves the result as a
new checkpoint, so a thread survives restarts when the storage is a
database. A run that stops for approval waits in the thread until
approve, reject or decide continues it; a failed or cancelled run
is saved too, and retry continues it.
Parameters
agent(Agent<Output>): The agent whose model, tools and storage the thread uses.id(string): The thread id in the agent's storage.actor(string, optional): Who makes the calls, recorded in audit records.
Example
import { Agent, FakeModel, FakeReply, tool } from "criogenai";
const refund = tool<{ orderId: string }>({
name: "refund",
description: "Refund an order.",
input: { type: "object", properties: { orderId: { type: "string" } }, required: ["orderId"] },
run: ({ orderId }) => `refunded ${orderId}`,
});
const agent = new Agent({
model: new FakeModel([FakeReply.toolCall("refund", { orderId: "A-1" }), "The order is refunded."]),
tools: [refund],
approve: ["refund"],
});
const thread = agent.thread("order-A-1");
const first = await thread.send("Refund order A-1.");
console.log(first.needsApproval, first.approvals[0].tool); // true "refund"
const done = await thread.approve();
console.log(done.text); // "The order is refunded."Thread.send#
send(input: string, options?: RunOptions): Promise<Reply<Output>>Runs one turn: the stored conversation plus input.
Parameters
input(string): The user's message.options(RunOptions, default{}):state(run state, e.g. the key per-user memory reads) andsignal(cancels the run; it is still saved).
Returns: The reply, saved as the thread's new head.
Raises
ThreadSuspendedError: The thread waits for approval: answer withapprove,rejectordecidefirst.ThreadBusyError: Another run holds the thread (see the storage'sonBusyoption).ModelError: The model failed after its retries and fallbacks;retrycontinues the saved run.LimitExceededError: A limit inlimitswas reached.CancelledError:signalwas aborted.
Thread.approve#
approve(options?: CallOptions): Promise<Reply<Output>>Approves every pending call and continues.
Parameters
options(CallOptions, default{}):signalcancels the run.
Thread.reject#
reject(message?: string, options?: CallOptions): Promise<Reply<Output>>Rejects every pending call (the model sees message).
Parameters
message(string, default"rejected"): What the model is told instead of each call's result.options(CallOptions, default{}):signalcancels the run.
Thread.decide#
decide(decisions: Record<string, Decision>, options?: CallOptions): Promise<Reply<Output>>One Decision per pending call, by call id.
Parameters
decisions(Record<string, Decision>): ADecisionfor each pending call, keyed by itscall_id(fromreply.approvals).options(CallOptions, default{}):signalcancels the run.
Example
import { Decision } from "criogenai";
const [request] = first.approvals; // first: the reply that needed approval
const done = await thread.decide({ [request.call_id]: Decision.reject("Refunds need a manager.") });Thread.retry#
retry(options?: CallOptions): Promise<Reply<Output>>Continues a failed or interrupted run. Tool calls that finished (and were saved) before the failure are not run again.
Parameters
options(CallOptions, default{}):signalcancels the run.
Thread.history#
history(limit?: number): Promise<Record<string, unknown>[]>Checkpoints, newest first.
Parameters
limit(number, default20): The most checkpoints to return.
Returns: One object per checkpoint of the current branch: id, parent_id, version, source, status, run_id, step, created_at_ms.
Thread.messages#
messages(): Promise<Record<string, unknown>[]>The stored conversation.
Thread.pendingApprovals#
pendingApprovals(): Promise<ApprovalRequest[]>The tool calls waiting for a decision, read from storage. Empty when the
thread is not waiting. Use it to show a review queue without keeping the
last Reply: for example after a restart, or in another process that
shares the storage.
Returns: One ApprovalRequest per waiting call, in call order.
Example
const waiting = await agent.thread("ticket-42").pendingApprovals();
for (const request of waiting) console.log(request.tool, request.args);Thread.rewind#
rewind(checkpointId: string): Promise<Record<string, unknown>>Makes an older checkpoint current again. It is saved as a new head; the newer checkpoints stay stored but leave the history. The next call runs from there, and nothing recorded after it is replayed.
Parameters
checkpointId(string): Anidfromhistory()or a reply'scheckpointId.
Returns: The new head's checkpoint, in the same shape as history() entries.
Thread.delete#
delete(): Promise<boolean>Deletes the thread and its checkpoints. true if it existed.
Thread.stream#
stream(input: string, options?: { state?: Record<string, unknown>; }): Stream<Output>send, streaming its events. To cancel, call stream.cancel() or leave
the for await loop early.
Parameters
input(string): The user's message.options({ state?: Record<string, unknown>; }, default{}):state: run state, as forsend.
Thread.streamApprove#
streamApprove(): Stream<Output>approve, streaming the events of the run it continues.
Thread.streamReject#
streamReject(message?: string): Stream<Output>reject, streaming the events of the run it continues.
Parameters
message(string, default"rejected"): What the model is told instead of each call's result.
Thread.streamDecide#
streamDecide(decisions: Record<string, Decision>): Stream<Output>decide, streaming the events of the run it continues.
Parameters
decisions(Record<string, Decision>): ADecisionfor each pending call, keyed by itscall_id.
Thread.streamRetry#
streamRetry(): Stream<Output>retry, streaming the events of the run it continues.
Stream#
class Stream<Output = unknown> implements AsyncIterable<StreamEvent> {
constructor(nativeStream: NativeStream, agent: Agent<Output>);
}A streamed thread call: for await (const event of stream), then
await stream.reply(). Leaving the loop early cancels the run (it is
still saved, and retry continues it).
Made by thread.stream(...) and the other thread.stream* methods.
Parameters
nativeStream(NativeStream): The engine's stream.agent(Agent<Output>): The agent the thread belongs to.
Example
import { Agent, FakeModel } from "criogenai";
const agent = new Agent({ model: new FakeModel(["Hello there."]) });
const stream = agent.thread("chat-1").stream("Hello");
for await (const event of stream) {
if (event.type === "text") process.stdout.write(event.text);
}
const reply = await stream.reply();Stream.reply#
reply(): Promise<Reply<Output>>Waits for the call; its saved reply.
Raises
CriogenError: The run failed (the same errors asthread.send).
Stream.cancel#
cancel(): voidCancels the run.
Reply#
interface Reply<Output = unknown> extends RunResult<Output> {
version: number;
checkpointId: string;
}A thread call's result, saved as the thread's new head.
Fields
version(number): The thread's version after this call; it grows with every saved checkpoint.checkpointId(string): The id of the checkpoint this call saved;thread.rewindtakes it.text(string | null): The final answer as text, ornullwhen the output is not text (or the run stopped for approval).output(Output | null): The final answer: the parsed JSON object with theoutputoption, else the final text.messages(Record<string, unknown>[]): The whole transcript: user, assistant and tool messages, in order.usage(Record<string, number>): Tokens across all model calls: input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens.stopReason(string | null): Why the run ended: "final_answer", "output" (typed output accepted), "return_direct", "content_filter", "hook_end" or "limit";nullwhile the run waits for approval.needsApproval(boolean):truewhen the run stopped because a tool call needs approval (seeapprovals).approvals(ApprovalRequest[]): The calls waiting for a decision; empty unlessneedsApproval.modelCalls(number): Model calls the run made (retries of one call do not count).toolCalls(number): Tool calls the run executed.runId(string): The run's id, also passed to tools (ctx.runId) and hooks.durationMs(number): Wall time of the call, in milliseconds.answeredBy({ provider: string; model: string; } | null): The provider and model that wrote the last answer (a fallback shows here).models(ModelUse[]): Each model this run called, in first-use order.
RunResult#
interface RunResult<Output = unknown> extends RunFacts {
text: string | null;
output: Output | null;
messages: Record<string, unknown>[];
usage: Record<string, number>;
stopReason: string | null;
needsApproval: boolean;
approvals: ApprovalRequest[];
modelCalls: number;
toolCalls: number;
runId: string;
}What a run produced; durationMs, answeredBy and models say which models answered and how long it took.
Fields
text(string | null): The final answer as text, ornullwhen the output is not text (or the run stopped for approval).output(Output | null): The final answer: the parsed JSON object with theoutputoption, else the final text.messages(Record<string, unknown>[]): The whole transcript: user, assistant and tool messages, in order.usage(Record<string, number>): Tokens across all model calls: input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens.stopReason(string | null): Why the run ended: "final_answer", "output" (typed output accepted), "return_direct", "content_filter", "hook_end" or "limit";nullwhile the run waits for approval.needsApproval(boolean):truewhen the run stopped because a tool call needs approval (seeapprovals).approvals(ApprovalRequest[]): The calls waiting for a decision; empty unlessneedsApproval.modelCalls(number): Model calls the run made (retries of one call do not count).toolCalls(number): Tool calls the run executed.runId(string): The run's id, also passed to tools (ctx.runId) and hooks.durationMs(number): Wall time of the call, in milliseconds.answeredBy({ provider: string; model: string; } | null): The provider and model that wrote the last answer (a fallback shows here).models(ModelUse[]): Each model this run called, in first-use order.
RunOptions#
interface RunOptions extends CallOptions {
state?: Record<string, unknown>;
}Options of agent.run and thread.send.
Fields
state(Record<string, unknown>, optional): Run state (e.g.{ user_id: "ada" }for per-user memory).signal(any, optional): Aborting cancels the run (a thread is still saved, and retry continues it).
CallOptions#
interface CallOptions {
signal?: AbortSignal;
}Options of every thread call that takes no input (approve, reject, decide, retry).
Fields
signal(any, optional): Aborting cancels the run (a thread is still saved, and retry continues it).
Decision#
type Decision =
| { type: "approve" }
| { type: "reject"; message?: string }
| { type: "edit"; args: Record<string, unknown>; name?: string }
| { type: "respond"; message: string };A reviewer's answer to one call (see Thread.decide).
Approve it, reject it (the model sees message), run it with edited
args (and optionally another tool name), or skip it and give the
model message as the tool's result. Decision.approve(),
Decision.reject(message), Decision.edit(args, name) and
Decision.respond(message) build them.
ApprovalRequest#
interface ApprovalRequest {
call_id: string;
tool: string;
args: Record<string, unknown>;
description?: string;
allowed?: string[];
}A tool call waiting for a reviewer's decision. A run that stops for
approval lists these in reply.approvals; answer them with
thread.approve(), thread.reject() or thread.decide().
Fields
call_id(string): The tool call's id.thread.decidetakes decisions keyed by it.tool(string): The name of the tool the model wants to call.args(Record<string, unknown>): The arguments the model wrote for the call.description(string, optional): Text for the reviewer, when the approval rule provides one.allowed(string[], optional): The decisions the reviewer may make: "approve", "edit", "reject", "respond".
Retry#
interface Retry {
maxAttempts?: number;
initialBackoffSecs?: number;
factor?: number;
maxBackoffSecs?: number;
jitter?: boolean;
}How often and how patiently to retry; missing fields keep the defaults.
Fields
maxAttempts(number, optional): Attempts in total, including the first. Default 3.initialBackoffSecs(number, optional): Backoff before the second attempt, in seconds. Default 0.5.factor(number, optional): Multiplier for each further attempt. Default 2.maxBackoffSecs(number, optional): Upper bound on any backoff, in seconds. Default 20.jitter(boolean, optional): Wait a random part of the backoff. Default true.
AgentTool#
class AgentTool {
constructor(agent: Agent<any>, name: string, description: string);
}An agent as a tool of another agent (agent.asTool): it takes { input } and returns the sub-agent's answer.
Parameters
agent(Agent<any>): The agent that runs when the tool is called.name(string): The tool name the calling model sees.description(string): What the tool does, for the calling model.
StreamEvent#
type StreamEvent =
| { type: "text"; messageId: string; text: string }
| { type: "tool_call_started"; callId: string; tool: string; args: Record<string, unknown> }
| { type: "tool_call_finished"; callId: string; tool: string; status: string }
| { type: "message"; message: Record<string, unknown> }
| { type: "approval_needed"; requests: ApprovalRequest[] }
| { type: "other"; event: string; node?: string | null; step?: number | null };One streamed event.
text (a piece of the answer as it is written), tool_call_started,
tool_call_finished, message (a finished message of the transcript),
approval_needed, or other (any other engine event: its name, e.g.
"step_started", and the node and step; never any content).