jonaki

Quick start

An agent with a tool, in about twenty lines — then the test that keeps it honest.

The smallest thing that works

hello.ts
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.

agent.ts
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.0078

Turn it into a test

Record one real run:

JONAKI_RECORD=runs npx tsx --env-file=.env agent.ts

That writes runs/runs/<runId>.json — the trace and the model exchanges in one file. Now replay it, with no API key and no network:

agent.test.ts
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

On this page