Quick start
An agent with a tool, in about twenty lines — then the test that keeps it honest.
The smallest thing that works
import { Agent, run } from "@rohhitrz/jonaki";
const result = await run(new Agent({ name: "Assistant" }), "Say hello.");
console.log(result.output);name is the only required field. The provider and model resolve from your
environment.
Add a tool
A tool is a Zod schema plus a function. The schema becomes JSON Schema for the model, and arguments are validated before your code runs.
import { Agent, run, tool } from "@rohhitrz/jonaki";
import * as z from "zod";
const getWeather = tool({
name: "get_weather",
description: "Get the current weather for a city.",
input: z.object({
city: z.string().describe("City name, e.g. 'Oslo'"),
}),
run: async ({ city }) => `${city}: 22°C, clear skies`,
});
const agent = new Agent({
name: "Assistant",
instructions: "You are a helpful assistant. Be concise.",
tools: [getWeather],
});
const result = await run(agent, "What's the weather in Oslo?");
console.log(result.output);
console.log(`${result.state.turn} turns · $${result.usage.costUSD.toFixed(4)}`);Note there is no type annotation on { city }. It is inferred from
input — hover it in your editor and TypeScript knows it is string.
See what happened
Every run carries its own trace. Print it as a tree:
import { formatTrace } from "@rohhitrz/jonaki";
console.log(formatTrace(result.state, { color: true }));run_wNKbuGgKMgH9 completed agent=Assistant
+0ms ◆ model claude-sonnet-4-5 [786→96 tok · $0.0038] → get_weather
+1ms ✔ get_weather {"city":"Oslo"} (1ms) → Oslo: 22°C, clear skies
+1.6s ◆ model claude-sonnet-4-5 [951→77 tok · $0.0040]
+1.6s ■ done It's 22°C and clear in Oslo.
total · 2 turns · 2 model · 1 tools · 1.6s · 1737 in / 173 out · $0.0078Turn it into a test
Record one real run:
JONAKI_RECORD=runs npx tsx --env-file=.env agent.tsThat writes runs/runs/<runId>.json — the trace and the model exchanges in one
file. Now replay it, with no API key and no network:
import { describe, expect, it } from "vitest";
import { replay } from "@rohhitrz/jonaki";
import agent from "./agent.js";
describe("weather agent", () => {
it("calls get_weather with the city the user asked about", async () => {
const result = await replay("runs/runs/booking.json", { agent });
const toolStep = result.steps.find((step) => step.type === "tool");
expect(toolStep).toMatchObject({ name: "get_weather", status: "ok" });
});
});Your tools still execute for real — only the model is replayed. Change a tool's implementation, replay, and watch what breaks.
Export the agent
For replay() and npx jonaki test to find your agent, export it as the
default export from the module: export default agent;
Where to go next
- Agents — every configuration option
- Tools — validation, timeouts, approval
- Replay testing — the full testing story
- Examples — six runnable projects