jonaki

API reference

Every export, every option, every type — enough that you never need to open the source.

Everything is imported from the package root. There are no deep imports.

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

Exports at a glance

Classes — Agent, JonakiError, MemoryStore, FileStore, SQLiteStore

Functions — tool, run, stream, resume, replay, approve, reject, redactPII, blockPatterns, requireApprovalFor, userMessagesOnly, lastMessages, handoffToolName, formatTrace, summarizeTrace, createSQLiteStore, createAnthropicProvider, createOpenAIProvider, createGoogleProvider, resolveProvider

Constants — JONAKI_ERROR_CODES


new Agent(config)

Static configuration. Never mutated by a run.

class Agent<TOutput = string> {
  constructor(config: AgentConfig<TOutput>);

  readonly name: string;
  readonly instructions?: string;
  readonly models: string[];
  readonly tools: AnyTool[];
  readonly handoffs: HandoffConfig[];
  readonly guardrails: Guardrail[];
  readonly output?: z.ZodType<TOutput>;
  readonly maxTurns?: number;
  readonly maxCostUSD?: number;
  readonly timeoutMs?: number;
  readonly provider?: Provider;
  readonly context?: ContextOptions;

  toolByName(name: string): AnyTool | undefined;
  handoffSpecs(): ToolSpec[];
  handoffForToolName(toolName: string): HandoffConfig | undefined;
  handoffConfigFor(agentName: string): HandoffConfig | undefined;
  toolSpecs(): ToolSpec[];
  registry(into?: Map<string, Agent<unknown>>): Map<string, Agent<unknown>>;
}

AgentConfig<TOutput>

FieldTypeDefault
namestringrequired
instructionsstring—
modelstring | string[]env-resolved
toolsAnyTool[][]
handoffsHandoffInput[][]
guardrailsGuardrail[][]
outputz.ZodType<TOutput>—
maxTurnsnumber10
maxCostUSDnumber—
timeoutMsnumber—
providerProviderenv-resolved
contextContextOptionsno windowing

Throws INVALID_CONFIG when name is missing.


tool(config)

function tool<TIn extends z.ZodType, TOut>(
  config: ToolConfig<TIn, TOut>,
): Tool<TIn, TOut>;

ToolConfig<TIn, TOut>

FieldTypeDefault
namestringrequired
descriptionstringrequired
inputTIn extends z.ZodTyperequired
run(args: z.infer<TIn>, ctx: ToolContext) => TOut | Promise<TOut>required
timeoutMsnumber30000
needsApprovalboolean | (args: z.infer<TIn>) => booleanfalse

Tool<TIn, TOut>

interface Tool<TIn extends z.ZodType = z.ZodType, TOut = unknown> {
  readonly name: string;
  readonly description: string;
  readonly input: TIn;
  readonly parameters: object;      // JSON Schema
  readonly timeoutMs: number;

  needsApproval(args: z.infer<TIn>): boolean;
  execute(rawArgs: unknown, ctx: ToolContext): Promise<TOut>;
  toSpec(): ToolSpec;
}

AnyTool is Tool<z.ZodType, unknown>, for heterogeneous arrays.

ToolContext

interface ToolContext {
  runId: string;
  agentName: string;
  signal: AbortSignal;
  log: (message: string, data?: Record<string, unknown>) => void;
}

run(agent, input, options?)

function run<TOutput = string>(
  agent: Agent<TOutput>,
  input: string | RunState,
  options?: RunOptions,
): Promise<RunResult<TOutput>>;

input is a string, or a RunState to continue.

RunOptions

OptionTypeDefault
runIdstringgenerated
maxTurnsnumberagent's, else 10
maxCostUSDnumberagent's
timeoutMsnumberagent's
maxHandoffDepthnumber3
signalAbortSignal—
storeStore—
sessionstring— (requires store)
sessionLimitnumberall
contextContextOptionsagent's
onEvent(event: JonakiEvent) => void—
streambooleanfalse
emitterEventEmittercreated
providerProviderenv-resolved
modelstring | string[]agent's
now() => numberDate.now
recordbooleanJONAKI_RECORD env

RunResult<T>

interface RunResult<T = unknown> {
  runId: string;
  status: RunStatus;   // "completed" | "awaiting_approval" | "failed"
  output: T;           // undefined when awaiting approval
  text: string;        // raw final assistant text
  state: RunState;
  usage: Usage;
  steps: Step[];
}

stream(agent, input, options?)

function stream<TOutput = string>(
  agent: Agent<TOutput>,
  input: string | RunState,
  options?: RunOptions,
): RunStream<TOutput>;

interface RunStream<TOutput> extends AsyncIterable<JonakiEvent> {
  result(): Promise<RunResult<TOutput>>;
}

Defaults stream to true, so text_delta events are emitted.


resume(runId, store, options)

function resume<TOutput = string>(
  runId: string,
  store: Store,
  options: ResumeOptions,
): Promise<RunResult<TOutput>>;

interface ResumeOptions extends RunOptions {
  agent: Agent<unknown>;  // required — config is code, never persisted
  approved?: boolean;
  note?: string;
}

Throws STORE_ERROR for an unknown runId or a mismatched agent, and TOOL_FAILED if a paused run is resumed without approved.


replay(path, options)

function replay<TOutput = string>(
  path: string,
  options: ReplayOptions,
): Promise<ReplayResult<TOutput>>;

interface ReplayOptions extends Omit<RunOptions, "provider" | "store"> {
  agent: Agent<unknown>;  // required
  input?: string;         // defaults to the recorded first user message
}

interface ReplayResult<TOutput = string> extends RunResult<TOutput> {
  recorded: RunState;     // the run this was compared against
}

Throws REPLAY_DIVERGED when the recording no longer fits the code.


Approval

function approve(state: RunState, note?: string): RunState;
function reject(state: RunState, reason?: string): RunState;

Both return a new state with the decision appended to the trace. A no-op when nothing is pending.


Guardrails

interface Guardrail {
  name: string;
  stage: GuardrailStage;  // "input" | "output" | "tool"
  check(
    payload: GuardrailPayload,
    ctx: GuardrailContext,
  ): GuardrailResult | Promise<GuardrailResult>;
}

type GuardrailPayload =
  | { stage: "input"; text: string }
  | { stage: "output"; text: string; output: unknown }
  | { stage: "tool"; toolName: string; args: unknown };

type GuardrailResult =
  | { action: "allow" }
  | { action: "block"; reason: string }
  | { action: "modify"; payload: unknown }
  | { action: "require_approval"; reason: string };

interface GuardrailContext {
  runId: string;
  agentName: string;
  turn: number;
}

Built-ins

function redactPII(options?: {
  stage?: GuardrailStage;   // default "input"
  name?: string;            // default "redact-pii"
  kinds?: string[];         // subset of ["email", "creditCard", "phone"]
  replacement?: (kind: string, match: string) => string;
}): Guardrail;

function blockPatterns(
  patterns: (string | RegExp)[],
  options?: {
    stage?: GuardrailStage; // default "input"
    name?: string;          // default "block-patterns"
    reason?: (match: string) => string;
  },
): Guardrail;

function requireApprovalFor(
  patterns: (string | RegExp)[],
  options?: { stage?: GuardrailStage; name?: string }, // default stage "tool"
): Guardrail;

Handoffs

type HandoffInput = Agent<unknown> | HandoffConfig;

interface HandoffConfig {
  agent: Agent<unknown>;
  toolName?: string;      // default `transfer_to_<snake_name>`
  description?: string;
  contextFilter?: (messages: Message[], ctx: HandoffContext) => Message[];
}

interface HandoffContext {
  runId: string;
  from: string;
  to: string;
  reason?: string;
}

function handoffToolName(agentName: string): string;
function userMessagesOnly(messages: Message[]): Message[];
function lastMessages(count: number): (messages: Message[]) => Message[];

Stores

interface Store {
  saveRun(state: RunState): Promise<void>;
  loadRun(runId: string): Promise<RunState | null>;
  appendSession(sessionId: string, messages: Message[]): Promise<void>;
  loadSession(sessionId: string, limit?: number): Promise<Message[]>;
  listRuns(filter?: ListRunsFilter): Promise<RunSummary[]>;
}

interface ListRunsFilter {
  sessionId?: string;
  limit?: number;
}

interface RunSummary {
  runId: string;
  agentName: string;
  status: RunStatus;
  sessionId?: string;
  turn: number;
  usage: Usage;
  createdAt: number;
  updatedAt: number;
}

Implementations

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

const memory = new MemoryStore();
const files = new FileStore({ dir: "runs" });
const sqlite = await createSQLiteStore({ path: "./app.db" });

sqlite.close();
ConstructorOptions
new MemoryStore(options?)now?: () => number
new FileStore(options)dir: string (required), now?: () => number
await createSQLiteStore(options?)path?: string (default ":memory:"), db?: SqliteDatabase to inject an open handle, now?: () => number

FileStore also exposes runPath(runId), sessionPath(sessionId), saveRecording(runId, recording), and loadRecording(runId). SQLiteStore exposes .db and .close().


Context

interface ContextOptions {
  maxTokens?: number;   // unset = no windowing
  keepRecent?: number;  // default 4
}

Errors

class JonakiError extends Error {
  constructor(code: JonakiErrorCode, options?: JonakiErrorOptions);

  readonly code: JonakiErrorCode;
  readonly details?: Record<string, unknown>;
  readonly hint?: string;
  readonly cause?: unknown;

  static is(value: unknown, code?: JonakiErrorCode): value is JonakiError;
}

interface JonakiErrorOptions {
  details?: Record<string, unknown>;
  hint?: string;
  cause?: unknown;
}

const JONAKI_ERROR_CODES: readonly JonakiErrorCode[];

See Error handling for every code and its fix.


State & trace

interface RunState {
  runId: string;
  agentName: string;
  status: RunStatus;
  messages: Message[];
  turn: number;
  handoffDepth: number;
  usage: Usage;
  pendingApproval?: {
    toolCall?: ToolCall;
    guardrail?: { name: string; stage: GuardrailStage; reason: string };
  };
  steps: Step[];
  output?: unknown;
  error?: { code: string; message: string };
  sessionId?: string;
  sessionOffset?: number;
}

type RunStatus = "running" | "awaiting_approval" | "completed" | "failed";

RunState is losslessly JSON round-trippable: no class instances, no functions, no Dates.

Steps

Every step has at: number (epoch ms) and turn: number.

type Step =
  | ModelStep      // provider, model, text, toolCalls, finishReason, usage
  | ToolStep       // toolCallId, name, args, status, result?, error?, durationMs
  | HandoffStep    // toolCallId, from, to, depth, reason?, messageIndex
  | GuardrailStep  // stage, name, action, reason?, toolName?
  | OutputRepairStep  // issues, received
  | ApprovalStep   // toolCallId, approved, note?
  | FinalizeStep;  // text

type ToolStepStatus = "ok" | "error" | "repair" | "rejected";

Formatting

function formatTrace(
  state: RunState,
  options?: { color?: boolean; maxValue?: number },
): string;

function summarizeTrace(state: RunState): TraceSummary;

interface TraceSummary {
  runId: string;
  agentName: string;
  status: string;
  turns: number;
  usage: Usage;
  durationMs: number;
  modelCalls: number;
  toolCalls: number;
  handoffs: number;
  failures: number;
}

Events

type JonakiEvent =
  | { type: "run_started"; runId; at; agentName; input }
  | { type: "model_call_started"; runId; at; agentName; provider; model; turn }
  | { type: "text_delta"; runId; at; delta }
  | { type: "model_call_completed"; runId; at; step: ModelStep }
  | { type: "tool_started"; runId; at; toolCallId; name; args }
  | { type: "tool_completed"; runId; at; step: ToolStep }
  | { type: "tool_failed"; runId; at; step: ToolStep }
  | { type: "handoff_started"; runId; at; from; to; reason? }
  | { type: "handoff_completed"; runId; at; step: HandoffStep }
  | { type: "guardrail_triggered"; runId; at; step: GuardrailStep }
  | { type: "approval_required"; runId; at; toolCall?; guardrail?; reason? }
  | { type: "run_completed"; runId; at; output; usage }
  | { type: "run_failed"; runId; at; code; message };

type JonakiEventType = JonakiEvent["type"];

Providers

interface Provider {
  id: ProviderId;  // "anthropic" | "openai" | "google"
  generate(model: string, req: ModelRequest): Promise<ModelResponse>;
  stream(model: string, req: ModelRequest): AsyncIterable<ProviderChunk>;
}

interface ModelRequest {
  messages: Message[];
  system?: string;
  tools?: ToolSpec[];
  temperature?: number;
  maxTokens?: number;
  signal?: AbortSignal;
}

interface ModelResponse {
  text: string;
  toolCalls: ToolCall[];
  usage: Usage;
  finishReason: FinishReason;  // "stop" | "tool_calls" | "length" | "error"
  raw: unknown;
}

type ProviderChunk =
  | { type: "text"; delta: string }
  | { type: "tool_call"; call: ToolCall }
  | { type: "done"; response: ModelResponse };

Factories

function createAnthropicProvider(options?: {
  apiKey?: string;
  baseURL?: string;
  client?: Anthropic;
}): Provider;

function createOpenAIProvider(options?: {
  apiKey?: string;
  baseURL?: string;
  client?: OpenAI;
}): Provider;

function createGoogleProvider(options?: {
  apiKey?: string;
  client?: GoogleGenAI;
}): Provider;

function resolveProvider(options?: {
  provider?: ProviderId | Provider;
  model?: string;
  env?: NodeJS.ProcessEnv;
}): { provider: Provider; model: string };

Shared types

type Role = "system" | "user" | "assistant" | "tool";

interface Message {
  role: Role;
  content: string;
  toolCalls?: ToolCall[];  // on assistant turns
  toolCallId?: string;     // on tool results
}

interface ToolCall {
  id: string;
  name: string;
  args: unknown;
}

interface ToolSpec {
  name: string;
  description: string;
  parameters: object;  // JSON Schema
}

interface Usage {
  inputTokens: number;
  outputTokens: number;
  costUSD: number;
}

CLI

npx @rohhitrz/jonaki init [--dir .] [--name <name>] [--provider <id>]
                [--tests|--no-tests] [--yes] [--force]

npx jonaki list  [--dir runs] [--db <path>] [--session <id>] [--limit 20]

npx jonaki trace <runId> [--dir runs] [--db <path>] [--json] [--no-color]

npx jonaki test  [dir] --agent <file>

jonaki test exits 1 when any replay fails.

Environment variables

VariableEffect
ANTHROPIC_API_KEYEnables the Anthropic provider
OPENAI_API_KEYEnables the OpenAI provider
GOOGLE_API_KEYEnables the Google provider
JONAKI_RECORDDirectory to record runs into; enables recording

On this page