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.0119Steps 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
| Type | Records |
|---|---|
model | provider, model, text, toolCalls, finishReason, usage |
tool | toolCallId, name, args, status, result?, error?, durationMs |
handoff | from, to, depth, reason?, messageIndex |
guardrail | stage, name, action, reason?, toolName? |
output_repair | issues, received |
approval | toolCallId, approved, note? |
finalize | text |
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 insteadjonaki trace finds a run by id even if the file was renamed, so a recording
saved as booking.json is still traceable.