Skip to content

TypeScript

The TypeScript SDK is a client for a running Curva server (curva serve, or a shared server). It has zero dependencies and uses the global fetch, so it runs on Node 18+, Deno, Bun and in browsers.

Terminal window
npm install curva-ai

The package is curva-ai on npm.

import { Curva, choice, score, noul } from "curva-ai";
const curva = new Curva(); // CURVA_BASE_URL (default http://localhost:7777), CURVA_API_KEY
const d = await curva.decide(
{ ticket: "I was charged twice for order A-104. Please refund the duplicate!" },
{
team: choice("Which team should handle this?", { billing: "payments, refunds", technical: "bugs", sales: "pricing" }),
frustration: score("How frustrated is the customer?", ["calm", "annoyed", "angry"]),
refund: noul("The customer explicitly asks for a refund"),
},
);
d.answers.team.choice; // "billing" | "technical" | "sales" | "none_of_these"
d.answers.team.confidence;
d.answers.frustration.score;
d.answers.refund.noul; // P(yes)

Answers are typed from the questions you pass: d.answers.team.choice is a union of that question’s option keys (plus "none_of_these"), so a typo is a compile error.

choice(instructions, options, { escape?, minConfidence?, examples?, think? })
score(instructions, levels, { minConfidence?, examples?, think? })
noul(instructions, { examples?, think? })
multi(instructions, options, { threshold?, think? })
dependsOn(question, ...keys) // answered after `keys`, seeing their answers; answers get `stage`

options is a list of keys or a { key: description } object. think: true lets that one question reason in a call of its own; dependsOn runs a workflow in one call.

new Curva({ baseUrl, apiKey, model, timeout: 30_000, maxRetries: 3, headers, fetch });
await curva.decide(state, questions, {
model: ["model-a", "model-b"], // or one of:
council: ["model-a", "model-b"], // blended; answers get `agreement`
cascade: ["cheap", "strong"], escalateBelow: 0.8, // answers get `answeredBy`
race: ["model-a", "model-b"], // first valid answer wins
mode: "auto", debias: true, project: "support", explain: true,
config: "curva-1.0.0", privacy: "strict",
});

Decisions use camelCase: latencyMs, costUsd, cached, requestId (the x-request-id header, for matching server logs).

await curva.feedback(d.id, "team", "billing");
await curva.calibration("team", "support");
await curva.audit({ project: "support", limit: 100 });
await curva.drift("team", { project: "support", weeks: 8 });
await curva.models();
await curva.health();

429, 5xx and dropped connections are retried, honouring Retry-After. Anything else throws a CurvaError (with status, type and message), or one of its subclasses: AuthError, InvalidRequestError, NotFoundError, RateLimitError and ModelError.

© 2026 Tarkova Private Limited.