jonaki

Tracing

The steps array is the trace — no second logging system, and secrets are scrubbed.

state.steps is the trace. There is no separate logger to configure or keep in sync.

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

const result = await run(agent, "What's the weather in Oslo?");

for (const step of result.steps) {
  console.log(step.type, step.at, step.turn);
}

Printing a run

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

console.log(formatTrace(result.state, { color: true }));
run_wNKbuGgKMgH9  completed  agent=Travel

+0ms     ◆ model claude-sonnet-4-5 [786→96 tok · $0.0038] → search_flights
+1ms     ✔ search_flights {"from":"Delhi","to":"Guwahati"} (1ms) → [{"flight":"AI-802"…
+1.6s    ◆ model claude-sonnet-4-5 [951→77 tok · $0.0040] → book_flight
+1.6s    ✔ book_flight {"flight":"6E-511","passenger":"Rohit Sharma"} (1ms) → Booked…
+3.7s    ■ done Done! I've booked flight 6E-511 departing Delhi at 13:05…

total  ·  3 turns  ·  3 model  ·  2 tools  ·  3.7s  ·  2801 in / 230 out  ·  $0.0119

Steps taken after a handoff are indented under the transfer that caused them.

Options: color (default false, so piped output stays clean) and maxValue (characters of args and results before truncating, default 80).

Totals

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

const summary = summarizeTrace(result.state);
// { runId, agentName, status, turns, usage, durationMs,
//   modelCalls, toolCalls, handoffs, failures }

Step types

TypeRecords
modelprovider, model, text, toolCalls, finishReason, usage
tooltoolCallId, name, args, status, result?, error?, durationMs
handofffrom, to, depth, reason?, messageIndex
guardrailstage, name, action, reason?, toolName?
output_repairissues, received
approvaltoolCallId, approved, note?
finalizetext

Every step also has at (epoch ms) and turn.

Querying the trace

Because it is plain data, ordinary array methods do the work:

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

// Which tools ran, and did any fail?
const tools = result.steps.filter((step) => step.type === "tool");
const failed = tools.filter((step) => step.status === "error");

// What did each model call cost?
for (const step of result.steps) {
  if (step.type === "model") {
    console.log(step.model, step.usage.costUSD);
  }
}

// The slowest tool
const slowest = tools.sort((a, b) => b.durationMs - a.durationMs)[0];

Secrets are scrubbed

API keys never reach a trace. Values are matched by pattern — sk-ant-…, sk-proj-…, AIza…, GitHub and Slack tokens, bearer tokens, JWTs — and by field name (apiKey, token, password, authorization, and similar). Scrubbing happens as steps are recorded and again before anything is persisted.

import { run, tool } from "@rohhitrz/jonaki";
import * as z from "zod";

const leaky = tool({
  name: "get_config",
  description: "Returns config.",
  input: z.object({}),
  run: () => ({ endpoint: "https://api.example.com", apiKey: "sk-ant-…" }),
});

// The trace stores: { endpoint: "https://api.example.com", apiKey: "[REDACTED]" }

Persisting traces

Any store keeps them. State is saved after every step, so a crashed run still leaves a readable trace:

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

const store = new FileStore({ dir: "runs" });
await run(agent, "hi", { store });

The CLI

npx jonaki list --dir runs              # recent runs, newest first
npx jonaki trace <runId> --dir runs     # one run as a tree
npx jonaki trace <runId> --dir runs --json   # the raw RunState
npx jonaki list --db ./app.db           # from SQLite instead

jonaki trace finds a run by id even if the file was renamed, so a recording saved as booking.json is still traceable.

On this page