Imported from khoshov/integration (
caveman-main/packages/sdk/typescript/AGENTS.md). Install upstream withnpx skills add khoshov/integration --skill typescript. Copyright stays with the author.
packages/sdk/typescript — TypeScript SDK (@caveman-ai/sdk)
Single-file SDK (src/index.ts) exported as an ES module. Provides Cave (main client),
CaveTrace (per-request tracing), and BM25-backed tool-search against the gateway. No runtime
dependencies — only devDependencies for TypeScript.
Layout
src/index.ts— entire SDK; exportsCave,CaveTrace,CaveOptions,CaveTool,ToolSearchResult,CompressOptions,CompressResult, and theContextPack*typestests/tool-search.test.ts— type-level assertions compiled bytsc --noEmittests/tool-search.runtime.mjs— runtime tests usingnode:test+ global fetch mock (imports fromdist/)tests/runtime-policy.runtime.mjs+tests/runtime-policy.test.ts— the runtime-policy client; drives every section of../../parity/runtime-policy.fixtures.json(fetch wire, signature cases, allassignment_vectorswith exact float equality, allguard_cases— the shared operator truth table lives in the fixture, not in this file — and alldecision_cases). Iterate the arrays; never hard-code their countstests/parity.runtime.mjs— cross-language conformance suite; drives../../parity/fixtures.json(shared with sdk-python). Same fixtures, two languages → a field in one SDK and not the other fails CI.tests/trace-continuity.runtime.mjs— trace/span id minting + which requests carryx-cave-trace-id/x-cave-parent-span-id; mirrors the Pythontests/test_trace_continuity.pytsconfig.json/tsconfig.test.json— separate configs; test config coverstests/. Both extend the repo-root../../../tsconfig.base.json.
Key APIs
new Cave(options)— requiresapiKey,baseURL,agentcave.trace(opts, fn)— wraps a callback with aCaveTrace; sends tool-call spans to/sdk/v1/events. The trace mintstraceId(32 lowercase hex) + a rootspanId(16 lowercase hex) with the exporter's RNG;opts.traceId/opts.spanIdcontinue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call made through the trace carriesx-cave-trace-id+x-cave-parent-span-id. Generic/sdk/v1/*calls and provider clients built off theCavecarry neither; the sole SDK-endpoint exception isCaveTrace.tool, whose/sdk/v1/eventscall carries the trace id and root parent span idCaveTrace.exporter({serviceName?})→ anOTelExporterwhosedefaultTraceIdis the trace's, so SDK spans and the gateway's request rows join one trace. MIRRORS the PythonTrace.exportercave.tools({ catalog, strategy })— returns{ initial, strategy, search(query, opts?) }.search()is async (returnsPromise<ToolSearchResult>); breaking change from 1.0 which was sync.opts.ranker("bm25"|"embeddings") is passed through to the gateway verbatim;opts.toolSessionIdsendssession_idso provider callbacks can re-inject called deferred tools. The SDK never computes similaritycave.toolSearch(catalog, query, opts?)— direct variant, same contract (incl.ranker/toolSessionId). Schema-token counters are estimates;tokenBasisdiscloses the counter andbasisis always"inferred"cave.compress(payload, opts?)→Promise<CompressResult>; POSTs/sdk/v1/compress, maps the Engine report. Byte-safe pass-through on any transport/parse problem (original input,ratio:0, no handle);tokenCountBasisdiscloses the counter andbasisis always"inferred". The SDK delegates — it never reimplements a compressorcave.context.pack(query, items, options)→Promise<ContextPackResult>; connected-only POST to/sdk/v1/context/pack. Lossy selector over caller-owned items, never CCR/ledger; returns exactdeferredIds. Transport or malformed-report failure returns all original items with zero inferred savingsCaveTrace.context.expand(sourceRef)— the GET half ofcheckpoint();GET /sdk/v1/checkpoints/{ref}/expandreturns the stored{source_ref, version, messages, checkpoint}cave.openai/anthropic/gemini/vertex()— thin provider clients; proxied through gateway; each exposes a.rawfetch escape hatch (mirrors the PythonProvider.raw).cave.bedrock({region, endpoint?})is a no-network first-party route descriptor: Runtime defaults to/bedrock; explicit Mantle returns/bedrock/anthropic;sdkOnly:falsemirrors Python'ssdk_onlycave.prompts.internalBrevity({style, preserveErrorsVerbatim?, preserveCodeVerbatim?})— output-style snippet (style:"none"→""); MIRRORS the Pythoncave.prompts.internal_brevityCaveTrace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})— passing alatencyClasshint sets thex-cave-asyncheader ("true"unless"interactive"); passingtoolSessionIdsetsx-cave-tool-session. Mirrored by the Pythonresponses.create(body, latency_class=..., tool_session_id=...)CaveTrace.artifacts.page()— sends versioned{value, options, workflow}; gateway stores only JSONvalue.artifacts.get(id)performs authenticated retrieval.strategy:"verbatim"bypasses storage. Mirrored by Python.CaveTrace.context.checkpoint()— POSTs to/sdk/v1/checkpoints; gateway persists it (Valkey) + returns a reversiblesource_ref(expand viaGET /sdk/v1/checkpoints/{ref}/expand)cave.exporter({serviceName?})→OTelExporter;recordSpan(...)maps current GenAI fields togen_ai.*,export()POSTs OTLP/JSON to standard/v1/traces(headers viaotlpHeaders(); legacy/otlp/v1/tracesremains server-only compatibility)cave.runtimePolicy({publicKey?, autoRefreshSeconds?, killEnv?})→RuntimePolicyClient.refresh()is the only network call (GET /sdk/v1/runtime-policy, std headers minus content-type); it Ed25519-verifies the bundle string's exact bytes before parsing, TOFU-pins the key the moment the signature verifies (before the schema/sequence checks, so a rejected-but-signed bundle cannot open a downgrade window), rejects a regressedsequence, and keeps last-known-good on any failure. The fetch carries a 30sAbortSignal.timeout(mirrors Python'stimeout=30); anautoRefreshSecondstick that lands mid-refresh is skipped, not stacked.decide(taskFamily, {unitKey, context, trace})is synchronous, local-only, and never throws; holdout suppresses onto the fallback path and a missing unit key or invalid experiment never guesses an arm.kill()latches locally,killEnvis re-read per decide,state()snapshots. Routing only — no savings vocabulary anywhere. MIRRORS the Pythoncave.runtime_policy()cave.retryLoopBreaker(threshold=3)→RetryLoopBreaker;.record(name, args)throwsRetryLoopErrorafterthresholdconsecutive identical tool calls;.guard(name, args, fn)records then runsfncave.jobs→ reservedJobsClientsurface. Every method fails locally withcave_async_jobs_unavailable; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the PythonCave.jobs
Conventions
- Tests: type assertions in
.test.ts(compiled only), runtime in.runtime.mjs(run againstdist/) - Build before runtime tests:
pnpm build && pnpm test:node - Request body keys are
snake_caseto the gateway; response mapped tocamelCaseinToolSearchResult x-cave-workflowheader defaults todefaultWorkflow ?? "unlabeled-workflow"; never omit it- Deferred tool-search session handoff uses request
session_id, resultsessionId, and provider headerx-cave-tool-session; update sdk-python + parity fixtures with any change
Gotchas
- byte-safe: SDK sends request bodies to the gateway verbatim; no rewriting allowed.
compress()is the one path that yields smaller bytes and it delegates to the Engine — on any problem it passes the original through - context packing is connected-only and intentionally lossy: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by
deferredIds. It chooses what enters window; cache-optimal assembly chooses placement - mirror sdk-python: every field/method exists in both, enforced by the shared parity suite — a divergence is a CI failure, not a convention slip. Change one SDK, change both and the fixtures
- published as
@caveman-ai/sdk; the workspace name stays@caveman-ai/sdkuntil the npm redirect plan lands strategy:"deferred"initial set =alwaysLoadtools + up toinitialToolCount(default 8); never returns the full catalog without asearch()callreductionPctrounds to one decimal;savedTokensis derived (full - sent), not from the gateway response
See ../../../CLAUDE.md (root)