caveman TypeScript SDK(@caveman-ai/sdk)实战解析:单文件零依赖客户端的追踪、工具搜索、压缩与跨语言一致性体系
【免费下载链接】caveman🪨 why use many token when few token do trick — Claude Code skill that cuts 65% of tokens by talking like caveman项目地址: https://gitcode.com/GitHub_Trending/caveman1/caveman
本文以packages/sdk/typescript/CLAUDE.md为核心骨架,结合src/index.ts(约 2400 行的单文件实现)、测试套件与跨语言 parity 契约,完整讲解@caveman-ai/sdk的目录布局、全部关键 API(Cave 客户端、CaveTrace 追踪、BM25 工具搜索、compress、context pack、checkpoints、runtime policy、retry loop breaker、jobs 预留面)、wire 约定与常见陷阱。读完你可以直接安装并使用该 SDK,理解它与 Caveman 网关之间的字节级契约,并知道每个"诚实性"设计(byte-safe 直通、basis: "inferred"、跨语言 parity 门禁)背后的源码依据。
1. 包概览:单文件、零运行时依赖、Node ≥ 22.13
SDK 是一个导出为 ES module 的单文件包,全部实现集中在 src/index.ts。从 package.json 可以确认其发布形态:
- 包名
@caveman-ai/sdk,当前版本 1.0.0,MIT 许可,"type": "module",入口为dist/index.js(类型声明dist/index.d.ts); - 零运行时依赖——
devDependencies中只有typescript@5.9.3;sideEffects: false; engines要求Node.js ≥ 22.13(依赖原生 fetch、AbortSignal、WebCrypto 的 Ed25519 验签等能力);- 关键脚本:
build(tsc)、test:types(tsc --project tsconfig.test.json,仅编译类型断言)、test:node(node --test tests/*.runtime.mjs,运行时测试针对编译产物dist/)。
安装与最小用法(来自 README.md):
npm install @caveman-ai/sdkimport { Cave } from "@caveman-ai/sdk"; const cave = new Cave({ apiKey: process.env.CAVE_API_KEY!, baseURL: "http://127.0.0.1:8787", agent: "support-agent", }); const result = await cave.compress("large payload"); console.log(result.output, result.basis); // basis is inferred连接类调用需要 Caveman 网关的 key;本地 Engine 压缩能力则走独立的 Caveman 运行时,与账号体系解耦。
2. 目录布局与测试双轨制
CLAUDE.md 给出的 Layout 小节是理解本包的钥匙:
| 文件 | 职责 |
|---|---|
| src/index.ts | 整个 SDK;导出Cave、CaveTrace、CaveOptions、CaveTool、ToolSearchResult、CompressOptions、CompressResult及全部ContextPack*类型 |
| tests/tool-search.test.ts | 纯类型级断言,由tsc --noEmit编译校验,不执行 |
| tests/tool-search.runtime.mjs | 运行时测试,node:test+ 全局 fetch mock,从dist/导入 |
| tests/runtime-policy.runtime.mjs + tests/runtime-policy.test.ts | runtime-policy 客户端;驱动 packages/sdk/parity/runtime-policy.fixtures.json 的每一节:fetch wire、签名用例、全部assignment_vectors(严格浮点相等)、全部guard_cases(共享算子真值表在 fixture 里而非测试文件里)以及全部decision_cases。约定是遍历数组,永远不硬编码数量 |
| tests/parity.runtime.mjs | 跨语言一致性套件;驱动 packages/sdk/parity/fixtures.json(与 sdk-python 共享)。同一份 fixture、两种语言——任何一端 SDK 多一个字段、少一个字段都会让 CI 变红 |
| tests/trace-continuity.runtime.mjs | trace/span id 铸造 + 哪些请求携带x-cave-trace-id/x-cave-parent-span-id;镜像 Python 的tests/test_trace_continuity.py |
| tsconfig.json / tsconfig.test.json | 两套配置,测试配置额外覆盖tests/;两者都继承仓库根的 tsconfig.base.json |
仓库实际测试目录还包含 assembly.runtime.mjs、cave-plan.runtime.mjs、context-pack.runtime.mjs、exporter.runtime.mjs、task-profile.runtime.mjs、tool-events.runtime.mjs、transport.runtime.mjs、packaging.runtime.mjs、shared-context.runtime.mjs、structural.runtime.mjs 等,覆盖了后文各 API 面。
核心约定(Conventions 节,必须原样保留):
- 测试双轨:
.test.ts只做类型断言(只编译不运行);.runtime.mjs是运行时测试(对dist/运行)。因此先构建再跑运行时测试:pnpm build && pnpm test:node。 - wire 方向:请求体键名对网关一律
snake_case;响应映射为camelCase(体现在ToolSearchResult上)。 x-cave-workflow头永不省略:默认取defaultWorkflow ?? "unlabeled-workflow"。- 延迟工具搜索的会话交接三处联动:请求体
session_id、结果sessionId、provider 请求头x-cave-tool-session。任何改动必须同步更新 sdk-python 与 parity fixtures。
3. Cave 客户端:构造与配置
new Cave(options)必填apiKey、baseURL、agent,源码在 src/index.ts。完整CaveOptions类型(L4)比文档清单更细,逐字段说明:
| 字段 | 说明 |
|---|---|
apiKey/baseURL/agent | 必填;baseURL必须是无凭据、无 query/fragment 的绝对 http(s) URL,尾部斜杠会被归一化 |
defaultWorkflow | 工作流标签;缺省时回落到CAVE_WORKFLOW环境变量,再回落到"unlabeled-workflow" |
retention | "metadata" \| "zdr" \| "configured" |
verifyOnInit | 可选布尔 |
controlURL | 保留给 control-api/api/v1/*面的独立地址,同样经过严格 URL 校验 |
user | 不透明的终端用户标识,作为x-cave-user-hash转发;它是原始值透传——若含 PII,调用方须自行先哈希 |
timeoutMs | 所有 SDK HTTP 请求的有限截止时间,默认 30 秒,必须是正安全整数 |
signal | 调用方级取消信号,应用于所有 SDK HTTP 请求 |
从源码结构看一个有趣的细节:构造函数通过globalThis读取CAVE_WORKFLOW(而非process),保持浏览器中立;环境变量值会先小写化并按[a-z0-9_-]{1,96}校验,非法值被忽略而不是让每个请求都 400——这让cave wrap --workflow x之类的包装器无需改代码即可给 SDK 应用的所有请求打标签,且显式传入的defaultWorkflow永远优先。
4. 追踪:cave.trace 与 trace 连续性
cave.trace(opts, fn)把回调包进一个CaveTrace:
- id 铸造规则:trace 用与 OTel exporter 相同的 RNG 铸造
traceId(32 位小写 hex)与根spanId(16 位小写 hex)。 - 续接入口 trace:
opts.traceId/opts.spanId用于续接入站 trace;形状不是精确 hex 的值会被替换而不是上线(源码中normalizeTraceId/normalizeSpanId保证)。 - 头注入的边界:只有通过 trace 发出的 provider 调用和 trace 作用域内的
/sdk/v1/*调用才携带x-cave-trace-id+x-cave-parent-span-id;直接挂在Cave上构建的 provider 客户端与 SDK 调用两者都不带。trace-continuity.runtime.mjs 专门验证这一点。 - 工具调用 span:
trace.tool(name, options, fn)在工具执行前后向POST /sdk/v1/events上报span_type: "tool.call"事件(序列号、耗时、ok/error 结果);该上报是尽力而为的元数据——telemetry 失败绝不能替换或吞掉工具的结果/错误(源码 L807-L817 的.catch(() => undefined))。 CaveTrace.exporter({serviceName?}):返回按服务名 memoized 的OTelExporter,其defaultTraceId就是本 trace 的 id——SDK 自己录的 span 和网关的请求行因此并入同一条 trace。runtime-policy 的 decision span 若传入CaveTrace,也复用同一个调用方可达的默认缓冲;调用trace.exporter().flush()即可把它们发出去。此面镜像 Python 的Trace.exporter。
5. 延迟工具搜索:BM25/嵌入排序 + 会话交接
这是 SDK 的核心省 token 面。两个入口共享同一契约:
cave.tools({ catalog, strategy })→ 返回{ initial, strategy, search(query, opts?) }。cave.toolSearch(catalog, query, opts?)→ 直接变体,适合你在外部管理 catalog 的场景。
关键行为(文档 + 源码 toolSearch 实现 印证):
search()是异步的(返回Promise<ToolSearchResult>)——这是对 1.0 的破坏性变更(旧版同步、返回CaveTool[]),调用方必须await。opts.ranker("bm25" | "embeddings")逐字透传给网关;SDK 从不算相似度(byte-safe、零依赖)。网关只有在接入了嵌入 provider 时才认"embeddings"。opts.toolSessionId发送session_id,让 provider 侧回调能把已被调用过的延迟工具重新注入。交接的完整链路是:请求体session_id→ 结果sessionId→ provider 头x-cave-tool-session。- strategy 语义(源码 L461-L493):
"all"→initial即整个 catalog;"deferred"→initial=alwaysLoad工具 + 至多initialToolCount个(默认 8)懒加载工具;search()再按需从网关拉取。永不带search()调用就返回全量目录。另有maxLoadedTools上限校验(必须 ≥ alwaysLoad 数量)。 - 结果映射:响应体
sent_schema_tokens/full_schema_tokens映射为sentSchemaTokens/fullSchemaTokens(camelCase);savedTokens是本地派生值(full - sent),不来自网关响应;reductionPct四舍五入到一位小数(Math.round(x * 1000) / 10)。schema token 计数器是估算,tokenBasis披露用的是什么计数器,basis恒为"inferred"。 - 防御性校验:响应里的工具名必须在本地 catalog 中存在且不重复,否则抛错——网关不能替你"发明"工具。
ToolSearchResult的完整字段(类型定义):sessionId?、tools、sentSchemaTokens、fullSchemaTokens、deferredCount、method、tokenBasis、basis: "inferred"、只读的savedTokens与reductionPct。
6. 压缩与上下文:compress / context.pack / checkpoint
6.1 cave.compress —— 唯一的"变小字节"路径,且必须委托
cave.compress(payload, opts?)→Promise<CompressResult>,POST /sdk/v1/compress,把 Engine 的报告映射出来。文档 Gotchas 强调的第一条就是byte-safe:SDK 把请求体逐字发给网关,不允许改写;compress()是唯一能产出更小字节的路径,而它委托给 Engine,从不自己实现压缩器。
失败语义(compress 实现 逐条印证):
- 任何传输/解析问题(非 2xx、响应缺
output字符串、token 计数非法、tokensAfter > tokensBefore)一律fail-closed 直通:output是原始输入、ratio: 0、无recoveryHandle; ratio不由服务端字段决定,而是从校验过的两个计数器重新计算——"重算而不是保留一个不一致或乐观的服务端字段";tokenCountBasis披露 Engine 用的计数器(如o200k_base或approx_chars_div_4),basis恒为"inferred"——SDK 从不发verified;- 成功时可选带出
recoveryHandle(恢复字节级原值的凭据)、method(如"toon"/"elision")、losslessToModel。
CompressOptions.contentType支持"json" | "toon" | "log" | "code" | "diff" | "search-result" | "text" | "toolschema"等提示,用于 Engine 的检测器。
6.2 cave.context.pack —— 连接态、有损的选择器
cave.context.pack(query, items, options)→Promise<ContextPackResult>,connected-onlyPOST /sdk/v1/context/pack。文档明确其定位:它决定什么进入模型窗口,而 cache-optimal assembly 决定被选内容放在哪——两者互补而非替代。要点:
- 它把条目字节发给网关,从不在本地 wrap 运行;是有损选择器,绝不通 CCR/ledger;
- 返回精确的
deferredIds(请求 id 减去选中 id,按请求顺序),调用方必须保留这些被延迟的条目以便补充; - 传输失败或报告畸形时,返回全部原始条目且
tokensSaved为 0("零推断节省",而不是谎称省了多少); - 源码中的完整性校验相当严格(contextPack 实现):
maxTokens必须为正、条目 id 不可重复、选中 id 必须属于请求集合、deferred_ids必须与期望集合逐项一致、tokensUsed + tokensSaved === tokensBefore、deferredCount === deferredIds.length,任一不满足即回落到 passthrough。
ContextPackItem支持id/text/ 可选tokens/ RFC 3339timestamp/priority/pin(pin为"必需上下文",钉住的条目放不下时调用返回诚实的 0);ContextPackOptions有maxTokens(必填 > 0)、reserveTokens、now、recencyHalfLifeMs、recencyWeight、errorBoost。
6.3 checkpoint 与 expand —— 可逆性是强制要求
CaveTrace.context.checkpoint():POST /sdk/v1/checkpoints,网关持久化(Valkey)并返回可逆的source_ref;CaveTrace.context.expand(sourceRef):GET 半程,GET /sdk/v1/checkpoints/{ref}/expand返回存储的{source_ref, version, messages, checkpoint}。文档的说法很直白:一个无法被 expand 的 checkpoint 就是 bug。
同一 trace 下还有artifacts面:CaveTrace.artifacts.page()发送版本化的{value, options, workflow}(网关注入x-cave-artifact-envelope: value-v1头),网关只存储 JSONvalue;非 verbatim 策略存储成功后返回带artifact_id的占位包裹文本;artifacts.get(id)执行鉴权取回;strategy: "verbatim"完全绕过存储直接返回原值。
7. Provider 客户端与其他 API 面
7.1 薄 provider 客户端
cave.openai()/cave.anthropic()/cave.gemini()/cave.vertex()都是经网关代理的薄客户端(前缀分别为/openai/v1、/anthropic、/gemini、/vertex),可选upstreamKey(Vertex 场景是 Google OAuth2 访问令牌,网关以Authorization: Bearer …转发);每个客户端都暴露.rawfetch 逃生舱(镜像 Python 的Provider.raw),且 raw 请求被约束在本 provider 前缀内。
cave.bedrock({ region, endpoint? })特殊:它是一个零网络的第一方路由描述符——endpoint 缺省"runtime"时gatewayPrefix为/bedrock,显式"mantle"时为/bedrock/anthropic,sdkOnly: false(镜像 Python 的sdk_only),不含任何 AWS 秘密。
7.2 其余关键 API(文档 Key APIs 节逐条对应)
cave.prompts.internalBrevity({style, preserveErrorsVerbatim?, preserveCodeVerbatim?}):输出风格片段,style: "none"返回空串(实现 L597-L600 就是一行三元表达式);CaveTrace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}}):传latencyClass会设置x-cave-async头("interactive"之外均为"true");传toolSessionId会设置x-cave-tool-session;cave.exporter({serviceName?})→OTelExporter:recordSpan(...)把当前 GenAI 字段映射为gen_ai.*,export()以 OTLP/JSONPOST到标准/v1/traces(头来自otlpHeaders();旧路径/otlp/v1/traces仅服务端兼容保留)。SpanOptions支持inputTokens/outputTokens/cachedTokens/costUsd等 provider 报告值;cave.cavePlan():GET /sdk/v1/cave-plan,以项目 key 的plan:read作用域鉴权,逐字返回 snake_case 计划——所有美元数字都是推断值且是每日费率,SDK 从不重推或按月投影;cave.sharedContext.put/get:会话键化的多 agent 共享上下文(POST/GET /sdk/v1/shared-context/{key}),网关按租户命名空间隔离。
7.3 retryLoopBreaker 与 jobs 预留面
cave.retryLoopBreaker(threshold = 3)→RetryLoopBreaker。.record(name, args)在连续第threshold + 1次相同(名称 + 稳定序列化参数签名)工具调用时抛RetryLoopError;.guard(name, args, fn)先 record 再执行fn;任何不同调用都会重置连击计数,.reset()可手动清零(实现 L664-L709)。cave.jobs是预留的JobsClient面:submit/status/cancel/wait/submitAndWait每个方法都在网络 I/O 之前本地失败,抛出code = "cave_async_jobs_unavailable"的AsyncJobsUnavailableError——在持久加密请求存储、凭据托管与可排空 worker 就位之前,不允许出现"假的已排队/已完成"。
8. runtimePolicy:签名校验、TOFU 与"永不抛异常"的路由决策
cave.runtimePolicy({publicKey?, autoRefreshSeconds?, killEnv?})→RuntimePolicyClient,是 SDK 中最重安全逻辑的一面(实现),文档每条断言都能在源码找到对应:
refresh()是唯一网络调用:GET /sdk/v1/runtime-policy,标准头减去content-type;请求挂 30 秒AbortSignal(镜像 Python 的timeout=30)。- 响应有界读取:
content-length超过 1 MiB 直接取消;流式读取累计超 1 MiB 抛oversized_response;fetch 实现没有有界可读 body 时抛bounded_response_unavailable,fail closed 而不是调用无界text()。 - Ed25519 验签在解析之前:对
bundle字符串的精确 UTF-8 字节验签(WebCrypto + RFC 8410 SPKI 前缀,任何异常都是false)。 - TOFU 钉住时机:签名验证通过的那一刻就钉住公钥——早于 schema_version / sequence 检查。否则会出现降级窗口:第一个签名 bundle 因 schema 被拒后,一个无签名的 bundle 反而被接受(verifyBundle 注释 L1222-L1266 把这段推理写得很完整)。
- schema 与单调性:只接受
caveman.runtime-policy.v1;sequence回退即拒(runtime policy sequence regressed);一切失败保留 last-known-good。 - autoRefresh 不堆叠:后台 tick 落在一次进行中的 refresh 上时跳过而不是排队。
decide(taskFamily, {unitKey, context, trace})同步、纯本地、永不抛:内部故障降级为baseline;kill()本地闩锁,killEnv(默认CAVEMAN_POLICY_KILL)每次 decide 重读(运行中可翻转),state()给快照。- 实验语义:holdout 先切出并强制落到 fallback("抑制,而非更危险的变体");缺
unitKey或实验配置非法时从不猜测 arm(no_unit_key/invalid_experiment);同一 family 在获胜层级有 >1 个候选时返回ambiguous_policy,宁缺毋猜。 - 确定性分桶:导出的
policyUnitFraction(...keys)是 Goshared/platform/sampling.Fraction的逐字节移植——每个 key 前缀 8 字节大端长度、SHA-256、取前 8 摘要字节做大端 uint64 右移 11 再除 2^53。导出它是为了让调用方能复现分派,也让跨语言 fixture 能按位钉住这个移植。 - 定位声明:只做路由,"到处没有节省词汇"(no savings vocabulary);
verify/budget/escalation对 SDK 是黑盒,原样透传给调用方。
其测试 runtime-policy.runtime.mjs(约 1176 行)驱动共享 fixture runtime-policy.fixtures.json 的全部用例节,是这一面"不靠口头保证"的落地方式。
9. 跨语言 parity:漂移即 CI 失败
文档 Gotchas 中最硬的一条是mirror sdk-python:每个字段/方法在两个 SDK 中都存在,由共享 parity 套件强制——"一次漂移是 CI 失败,不是约定失误;改了一个 SDK,必须同时改另一个和 fixture"。
packages/sdk/parity/CLAUDE.md 进一步说明这份契约如何执行:
- fixtures.json 是契约本体:一个
config、命名头集合(std_headers/std_headers_traced/std_headers_async_traced/otlp_headers)与有序operations列表;每个 operation 带input、预制response(或transport: "error"强制 byte-safe 直通)和expect块(wire 的 method/path/headers/body 或 body_keys + result); - TS 半边是 tests/parity.runtime.mjs(mock fetch),Python 半边是 sdk-python 的
tests/test_parity.py(mock urlopen);每半边对每一个operation 都有 handler,缺 handler 就是失败,不是跳过; - 编辑规则:给一个 SDK 加字段 → 在这里加 operation → 另一个 SDK 的半边变红,"那个红色正是目的所在";
- 头按 key 小写化比较(Python urllib 会大写化);fixture 值必须避开两语言编码不一致的字符(如
! ( ) *,JSencodeURIComponent与 Pythonquote分歧);随机值不得进入断言,SDK 本该铸造的 id 通过 operationinput注入并钉在期望头集合里。
这就是 CLAUDE.md 所谓"release gate, not documentation"的含义:它不是描述行为的文档,而是行为本身的裁判。
10. 陷阱清单与适用前提
把文档 Gotchas 节整理成一张可操作清单:
- byte-safe 是底线:SDK 对网关的请求体逐字发送,禁止改写;唯一变小字节的路径是
compress(),且它委托 Engine,任何异常直通原文。 - context packing 是 connected-only 且有意有损:发送条目字节到网关、从不在本地 wrap 运行、依赖调用方保留
deferredIds点名的每条;它选"什么进窗口",cache-optimal assembly 管"放哪里"。 - mirror sdk-python:单侧改动必须双侧 + fixture 同步。
- npm 名称现状:发布名
@caveman-ai/sdk,workspace 名在 npm 重定向方案落地前保持@caveman-ai/sdk。 - deferred 初始集=
alwaysLoad工具 + 至多initialToolCount(默认 8);不带search()调用永不返回全量 catalog。 - 数字语义:
reductionPct一位小数;savedTokens是派生值(full - sent),不是网关字段;SDK 全链路basis: "inferred",verified只属于 Cloudactive路径。
适用前提:Node ≥ 22.13、ES module 环境、连接类 API 需要网关 key;运行测试需先pnpm build && pnpm test:node。所有接口面(provider 客户端、compress、延迟工具搜索、可逆 checkpoint 与 artifacts、retry-loop 中断、runtime policy、零依赖 OTLP/JSON exporter)与 README.md 的清单一一对应,异步作业则如前所述为本地失败的预留面。
总结
@caveman-ai/sdk用约 2400 行单文件实现了一个"网关协作型" TypeScript 客户端:以Cave/CaveTrace两个入口覆盖追踪、工具延迟加载、压缩、上下文打包、checkpoint 与产物五大能力,以 byte-safe 直通、inferred基线、确定性分桶移植和 Ed25519 + TOFU 验签守住诚实性与安全性边界,再以与 Python SDK 共享的 parity fixture 把"两语言一个契约"变成 CI 门禁。对需要把 agent 接入 Caveman 网关、同时要求可观测与可验证节省语义的 TypeScript 项目,这套 API 与约定(尤其x-cave-workflow永省略除、snake_case 出/camelCase 入的 wire 方向、以及三处联动的 tool-session 交接)是完整的实现事实来源。
【免费下载链接】caveman🪨 why use many token when few token do trick — Claude Code skill that cuts 65% of tokens by talking like caveman项目地址: https://gitcode.com/GitHub_Trending/caveman1/caveman
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考