jonaki

Agents

Static configuration — an agent is defined once and never mutated by a run.

An Agent is static configuration. A run never mutates it, so one instance can safely serve many concurrent runs.

import { Agent } from "@rohhitrz/jonaki";

const agent = new Agent({ name: "Assistant" });

name is the only required field.

Options

FieldTypeDefaultNotes
namestring—Required. Identifies the agent in traces and handoff tool names.
instructionsstringnoneThe system prompt.
modelstring | string[]env-resolvedAn array is a fallback ladder, tried in order.
toolsAnyTool[][]Built with tool().
handoffsHandoffInput[][]See Handoffs.
guardrailsGuardrail[][]See Guardrails.
outputz.ZodType<T>noneMakes run() return RunResult<T>.
maxTurnsnumber10Model calls before MAX_TURNS_EXCEEDED.
maxCostUSDnumbernoneChecked before each model call.
timeoutMsnumbernoneCeiling for the whole run.
providerProviderenv-resolvedOverride the adapter entirely.
contextContextOptionsno windowingSee Memory & sessions.

Instructions

instructions becomes the system prompt.

import { Agent } from "@rohhitrz/jonaki";

const agent = new Agent({
  name: "Support",
  instructions: [
    "You are a support agent for an online bookshop.",
    "Be concise. Never invent order numbers.",
    "If you cannot help, say so plainly.",
  ].join("\n"),
});

Choosing a model

Pass the provider's own model id. Jonaki infers the provider from the prefix — see Model providers.

import { Agent } from "@rohhitrz/jonaki";

const agent = new Agent({ name: "A", model: "claude-sonnet-4-5" });

Fallback ladders

An array is tried in order until one succeeds. The ladder can cross providers, so an Anthropic outage falls through to OpenAI:

import { Agent } from "@rohhitrz/jonaki";

const agent = new Agent({
  name: "Resilient",
  model: ["claude-sonnet-4-5", "gpt-4.1", "gemini-2.5-flash"],
});

If every rung fails you get PROVIDER_ERROR listing what each one said.

Limits

Every limit has its own error code, and cost is checked before the call that would breach it rather than after:

import { Agent } from "@rohhitrz/jonaki";

const agent = new Agent({
  name: "Bounded",
  maxTurns: 6,        // MAX_TURNS_EXCEEDED
  maxCostUSD: 0.25,   // BUDGET_EXCEEDED
  timeoutMs: 30_000,  // RUN_TIMEOUT
});

Run-level options override the agent's:

import { run } from "@rohhitrz/jonaki";

await run(agent, "hi", { maxTurns: 3 });

Typed output

Give the agent a Zod schema and run() returns that type — no cast:

import { Agent, run } from "@rohhitrz/jonaki";

import * as z from "zod";

const Ticket = z.object({
  customer: z.string(),
  urgent: z.boolean(),
});

const extractor = new Agent({ name: "Extractor", output: Ticket });

const result = await run(extractor, "Priya is furious about order A-77.");
result.output.urgent; // boolean

See Structured output.

Reusing one agent

Because an agent is immutable, concurrency is free:

import { Agent, run } from "@rohhitrz/jonaki";

const agent = new Agent({ name: "Assistant" });

const answers = await Promise.all([
  run(agent, "What is 2 + 2?"),
  run(agent, "Name a colour."),
  run(agent, "Say hello."),
]);

Each run gets its own RunState; the agent itself is untouched.

Inspecting an agent

import { Agent } from "@rohhitrz/jonaki";

const agent = new Agent({ name: "Assistant", tools: [getWeather] });

agent.name;                       // "Assistant"
agent.tools.map((t) => t.name);   // ["get_weather"]
agent.toolSpecs();                // what the model is shown, handoffs included
agent.registry();                 // Map of every agent reachable via handoffs

On this page