TypeScript SDK
A single-file TypeScript client for the Caveman gateway. It traces workflows, searches tool catalogs, delegates compression, proxies provider calls, and builds OTLP JSON without runtime dependencies.
The workspace package is @caveman-cloud/sdk (ES module, no runtime dependencies). It wraps the /sdk/v1/* gateway contract. It is not a compressor, tokenizer, or ranker; those decisions stay in the gateway and engine.
It mirrors the Python SDK field-for-field against the same wire contract. The two are kept in lockstep by a shared parity suite — a field that exists in one and not the other fails CI.
Install
There is no published @caveman/sdk on npm yet (the name is reserved for launch). Today you build from source in the workspace.
# from the repo root (pnpm workspace)
pnpm --filter @caveman-cloud/sdk build
# build, then run the runtime tests against dist/
pnpm --filter @caveman-cloud/sdk build && pnpm --filter @caveman-cloud/sdk test:nodeThe build emits an ES module under dist/. Import the Cave client and the types you need:
import { Cave } from "@caveman-cloud/sdk";
import type { CompressResult, ToolSearchResult } from "@caveman-cloud/sdk";Workspace name vs. published name
The package is @caveman-cloud/sdk in the workspace; @caveman/sdk is the intended published name and is not live (it 404s as of this writing). Build from source until the npm redirect lands.
The client
new Cave(options) requires three fields — an API key, the gateway base URL, and an agent slug. The agent slug labels every span the SDK records.
import { Cave } from "@caveman-cloud/sdk";
const cave = new Cave({
apiKey: process.env.CAVE_API_KEY!,
baseURL: "http://127.0.0.1:8787",
agent: "my-service",
defaultWorkflow: "support-triage", // optional; defaults to "unlabeled-workflow"
});The constructor throws if apiKey, baseURL, or agent is missing. retention ("metadata" | "zdr" | "configured") and controlURL (the control-plane host the jobs client targets) are optional.
Tracing
cave.trace(opts, fn) wraps a unit of work. Inside the callback you get a CaveTrace whose tool(), model, artifacts, and context helpers attribute everything to the trace's workflow. Tool spans are POSTed to /sdk/v1/events; telemetry failures are swallowed so they never break your agent.
await cave.trace({ workflow: "refund-flow", tags: { tier: "pro" } }, async (trace) => {
const result = await trace.tool("lookupOrder", { readOnly: true, idempotent: true }, async () => {
return db.orders.find(orderId);
});
const reply = await trace.model.openai.responses.create({
model: "gpt-4.1",
input: `Summarize order ${orderId}`,
});
return reply;
});Checkpoints
trace.context.checkpoint(messages, options) POSTs a conversation to /sdk/v1/checkpoints; the gateway persists it and returns a reversible source_ref. trace.context.expand(sourceRef) is the other half — it GETs the stored { source_ref, version, messages, checkpoint } back. A checkpoint that cannot be expanded is a bug.
const cp = await trace.context.checkpoint(messages, { reason: "pre-summarization" });
// ... later, or in a peer step ...
const restored = await trace.context.expand(cp.source_ref as string);Artifacts
trace.artifacts.page(value, options) pages a large payload out to /sdk/v1/artifacts and returns a compact [cave-artifact …] stub the model can expand on demand. strategy: "verbatim" bypasses storage and returns the value unchanged.
const stub = await trace.artifacts.page(bigSearchResult, {
source: "vector-search",
contentType: "application/json",
strategy: "json-index",
});Tool search
cave.tools({ catalog, strategy }) returns a handle with { initial, strategy, search() }. strategy: "all" puts the whole catalog in initial; strategy: "deferred" puts only the alwaysLoad tools plus the first initialToolCount (default 8). Either way, search() calls the gateway with the full catalog.
const tools = cave.tools({ catalog: myTools, strategy: "deferred" });
// send tools.initial on the first turn
const found: ToolSearchResult = await tools.search("refund a charge", {
maxTools: 5,
ranker: "bm25",
});
console.log(found.tools, found.savedTokens, found.reductionPct);search() is async (a breaking change from 1.0, which was synchronous) — you must await it. The ranker ("bm25" | "embeddings") is passed through to the gateway verbatim; the gateway honors "embeddings" only when it has an embedding provider wired. The SDK never computes similarity itself. cave.toolSearch(catalog, query, opts?) is the flat variant for when you manage the catalog outside a handle.
Compression
cave.compress(payload, opts?) POSTs to /sdk/v1/compress and returns the Engine's report. The SDK delegates — it never reimplements a compressor, because that would fork behavior across surfaces.
const result: CompressResult = await cave.compress(bigJson, { contentType: "json" });
if (result.recoveryHandle) {
// dropped detail is recoverable byte-exact via the handle
}
console.log(result.ratio, result.basis); // e.g. 0.93, "inferred"Honesty rule
compress() is byte-safe. On any transport or parse problem it passes the original payload through unchanged: ratio is 0, there is no recoveryHandle, and basis is always "inferred". The SDK never rewrites bytes itself and never reports a saving the engine did not hand back. Because it mirrors the Python SDK field-for-field, divergence between them fails CI.
Provider clients
cave.openai(), cave.anthropic(), cave.gemini(), and cave.vertex() return thin clients proxied through the gateway. Each carries a .raw fetch escape hatch for native provider paths the typed helpers don't cover. cave.bedrock({ region }) returns a local descriptor (sdkOnly: true) and makes no HTTP call.
const openai = cave.openai({ upstreamKey: process.env.OPENAI_API_KEY });
const out = await openai.responses.create({ model: "gpt-4.1", input: "hello" });
// vertex upstreamKey is a Google OAuth2 access token
const vertex = cave.vertex({ upstreamKey: await gcloudAccessToken() });
// native Vertex path via the escape hatch
const native = await vertex.raw(
"/vertex/v1/projects/p/locations/us-central1/publishers/google/models/gemini-1.5-pro:generateContent",
{ method: "POST", body: JSON.stringify(body) },
);You can attach a latencyClass hint ("interactive" | "background" | "offline") to a responses.create call; anything other than "interactive" sets the x-cave-async header so the gateway can defer it.
The OTel exporter
cave.exporter({ serviceName? }) returns an OTelExporter bound to this client's gateway config. recordSpan() maps GenAI fields (provider, model, token counts, costUsd) to gen_ai.* semantic-convention attributes; export() POSTs the buffered OTLP/JSON batch to /otlp/v1/traces and clears the buffer. No external OpenTelemetry wiring, no runtime deps — the payload is built by hand.
const otel = cave.exporter({ serviceName: "billing-agent" });
const root = otel.recordSpan("chat.completion", {
provider: "openai",
model: "gpt-4.1",
inputTokens: 1200,
outputTokens: 340,
status: "ok",
});
otel.recordSpan("tool.call", { toolName: "lookupOrder", parentSpanId: root.spanId });
await otel.export(); // → { ok, spans_accepted, spans_total, ... }The retry-loop breaker
cave.retryLoopBreaker(threshold = 3) returns a RetryLoopBreaker that interrupts a stuck agent re-issuing the same tool call. Call record(name, args) before each invocation (or wrap it with guard()); once the identical (name, args) signature repeats past the threshold it throws RetryLoopError. Any different call resets the streak.
const breaker = cave.retryLoopBreaker(3);
try {
await breaker.guard("search", { q: "refund" }, () => runSearch("refund"));
} catch (e) {
if (e instanceof RetryLoopError) {
// the agent was looping — break out and re-plan
}
}Async jobs
cave.jobs targets /api/v1/jobs on options.controlURL and falls back to baseURL. submit() returns { id, state }, status() re-reads it, wait() polls to a terminal state, submitAndWait() does both, and cancel() requests cancellation. This is for gateway deployments that expose that public API path.
const job = await cave.jobs.submitAndWait(
{ kind: "report.export", project_id: projectId },
{ latencyClass: "background", timeoutMs: 120_000 },
);
console.log(job.state); // "completed" | "failed" | "cancelled"