Caveman TypeScript SDK(@caveman-ai/sdk) 深度解析:Cave 客户端、延迟工具检索、字节安全压缩与本地运行时策略
本文为 Caveman 项目 TypeScript SDK 开发指引 的完整技术展开:以单文件、零运行时依赖的 @caveman-ai/sdk 为对象,讲清 Cave 主客户端、CaveTrace 请求级追踪、BM25 服务端工具检索、字节安全的 compress() 委托压缩、可逆 checkpoint/上下文打包、依赖免费的 OTLP/JSON 导出器,以及带 Ed25519 签名验证的 RuntimePolicyClient 的完整契约与实现细节。读完本文,你可以把该 SDK 接入自己的 Agent 网关调用链,并理解它在 wire 协议、fail-closed 语义与跨语言 parity 上做出的每一个设计取舍。
一、SDK 定位:单文件、零运行时依赖、ES Module
package.json 定义了该包的发布形态与硬约束:
- 包名
@caveman-ai/sdk,当前版本1.0.0;CHANGELOG.md 记录了 1.0.0(2026-07-26)“记录了稳定的 TypeScript SDK API 与/sdk/v1/*wire 基线,并与 Python SDK 锁定 coordinated-major 与 parity 规则”; "type": "module",入口dist/index.js、类型dist/index.d.ts,"sideEffects": false,发布物只有dist/、README 和 LICENSE;- engines 要求 Node.js ≥ 22.13(README.md 同样强调此要求);
- 零运行时依赖:
dependencies为空,devDependencies只有typescript@5.9.3; - 脚本:
build=tsc;test:types用tsconfig.test.json做纯类型编译;test:node跑node --test --test-force-exit tests/*.runtime.mjs;完整test会先 build 再依次跑类型测试与运行时测试。
整个 SDK 的实现只有一个文件 src/index.ts(约 2400 行),导出 Cave(主客户端)、CaveTrace(请求级追踪)、CaveOptions、CaveTool、ToolSearchResult、CompressOptions、CompressResult 以及 ContextPack* 系列类型。零依赖的直接后果是:SDK 内部所有加密(WebCrypto Ed25519/SHA-256)、base64 解码、URL 解析都自己实现,不引入任何第三方包。
二、目录结构与测试分层
AGENTS.md 给出的 Layout 与仓库实际一一对应,测试按“类型断言 / 运行时行为”严格分层:
| 路径 | 职责 |
|---|---|
| src/index.ts | 整个 SDK 的实现与全部公开导出 |
| 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 | 驱动 packages/sdk/parity/ 下与 Python SDK 共享的 runtime-policy fixture:fetch wire、签名用例、全部 assignment_vectors(精确浮点相等)、全部 guard_cases(算子真值表放在 fixture 里而非测试文件中)、全部 decision_cases;要求“迭代数组,永远不要硬编码数量” |
| tests/parity.runtime.mjs | 跨语言一致性套件,驱动共享 fixtures.json —— 同一组 fixture 两个语言,任一 SDK 缺字段即 CI 失败 |
| tests/trace-continuity.runtime.mjs | trace/span id 铸造规则 + 哪些请求携带 x-cave-trace-id / x-cave-parent-span-id,镜像 Python 侧 test_trace_continuity.py |
| tsconfig.json / tsconfig.test.json | 两份独立配置,测试配置覆盖 tests/;两者都继承仓库根目录的 tsconfig.base.json |
其余运行时测试(如 tests/exporter.runtime.mjs、tests/context-pack.runtime.mjs、tests/assembly.runtime.mjs)分别覆盖后文各 API 的行为契约。开发约定是:先构建再跑运行时测试 —— pnpm build && pnpm test:node。
三、Cave 主客户端:连接参数与校验语义
new Cave(options) 是入口,apiKey、baseURL、agent 三者必填,缺失即抛错(index.ts#L350-L376)。CaveOptions 的完整字段(index.ts#L4):
| 字段 | 说明 |
|---|---|
apiKey / baseURL / agent |
必填。连接 Caveman 网关的项目密钥、网关地址与 agent 标识 |
defaultWorkflow |
默认工作流标签;每个请求的 x-cave-workflow 头都不得省略 |
retention |
"metadata" | "zdr" | "configured",数据保留策略声明 |
verifyOnInit |
构造时是否执行连接校验 |
controlURL |
可选的 control-api(/api/v1/*)地址,与 baseURL 分离 |
user |
不透明的终端用户标识,以 x-cave-user-hash 转发;若是 PII 需自行先哈希 |
timeoutMs |
所有 SDK HTTP 请求的总截止期,默认 30 秒,必须是正整数 |
signal |
调用方取消信号,作用于所有 SDK HTTP 请求 |
两个值得注意的校验/归一化行为:
- URL 严格校验:
normalizedServiceURL()要求baseURL/controlURL是绝对 http(s) URL、无内嵌凭据、无 query/fragment、首尾无空白,并剥掉末尾斜杠(index.ts#L335-L348)。 CAVE_WORKFLOW环境变量兜底:未显式设置defaultWorkflow时,构造函数会读取CAVE_WORKFLOW并归一化为网关标签规则(小写[a-z0-9_-],最长 96);不合法的环境值被静默忽略而不是让每个请求 400(index.ts#L358-L375)。源码注释说明其用途:cave wrap --workflow x这类包装器可以为整条应用链路打标签而无需改代码。
最小可用示例来自 README.md:
import { 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
四、CaveTrace:trace 连续性与 ID 铸造规则
cave.trace(opts, fn) 把回调包进一个 CaveTrace(index.ts#L391-L393),它承担三件事:
- 铸造连续 ID:
traceId为 32 位小写十六进制、根spanId为 16 位小写十六进制,由 OTel 导出器同款 RNG 生成。若opts.traceId/opts.spanId用于续接入站 trace,则形状不符的值会被替换而不是发上 wire(防止把任意字符串放进请求头)。 - 给经过 trace 的每个 provider 调用加头:凡是
CaveTrace发起的 provider 请求都携带x-cave-trace-id+x-cave-parent-span-id;而直接走Cave构造的通用/sdk/v1/*调用与 provider 客户端两者都不带。唯一的 SDK 端点例外是CaveTrace.tool,它的/sdk/v1/events调用会携带 trace id 与根 parent span id。 - 工具调用埋点:
trace.tool(name, options, fn)在回调前后向/sdk/v1/events发送span_type: "tool.call"事件(含outcome: "ok"|"error"、sequence、duration_ms、tags),且best-effort —— 遥测失败永远不覆盖工具结果或异常(index.ts#L793-L817)。
CaveTrace.exporter({serviceName?}) 返回的 OTelExporter 的 defaultTraceId 就是该 trace 的 id —— 这是让“SDK 自己记录的 span”与“网关落库的请求行”汇入同一条 trace 的机制(index.ts#L784-L791);同一 service name 重复调用返回同一个 buffer(含 runtime-policy 决策 span),便于调用方统一 flush。该 API 镜像 Python 的 Trace.exporter。
trace 内还暴露了两类便捷面:
trace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}}):传latencyClass会设置x-cave-async头(值非"interactive"时为"true");传toolSessionId会设置x-cave-tool-session头(index.ts#L824-L829)。trace.artifacts.page(value, options)/trace.artifacts.get(id):page 发送版本化信封(头x-cave-artifact-envelope: value-v1)到/sdk/v1/artifacts,gateway 只存储 JSONvalue,成功时返回一个含artifact_id的可检索占位块;strategy:"verbatim"则绕过存储直接原样返回;stored !== true或缺artifact_id时抛错而不是猜(index.ts#L831-L845)。
五、延迟工具检索:tools() 与 toolSearch()
工具目录句柄 cave.tools({ catalog, strategy, initialToolCount?, maxLoadedTools? }) 返回 { initial, strategy, search(query, opts?) }(index.ts#L468-L520):
- strategy
"all"(默认):initial即整个目录;search()依然请求服务端。 - strategy
"deferred":initial= 所有alwaysLoad工具 + 至多initialToolCount(默认 8)个非 alwaysLoad 工具;maxLoadedTools若设置,必须不小于 alwaysLoad 工具数,且 lazy 配额取min(initialToolCount, maxLoadedTools - alwaysLoadCount)。任何情况下都不返回全量目录——必须显式调用search()。
search() 的关键契约:
- 1.0 破坏性变更:
search()是async,返回Promise<ToolSearchResult>(此前是同步返回CaveTool[]),调用方必须await; opts.ranker("bm25"|"embeddings")被原样透传给网关;SDK 自身从不计算相似度 —— 这是“byte-safe + 零依赖”原则的一部分;opts.toolSessionId以请求体session_id下发,使 provider 回调能把“已调用的延迟工具”重新注入;响应sessionId对应 wire 的session_id,provider 侧头为x-cave-tool-session;- 返回值的 token 口径:
sentSchemaTokens/fullSchemaTokens是估算,tokenBasis披露所用计数器,basis恒为"inferred";savedTokens是本地派生量(full - sent),reductionPct四舍五入到一位小数(index.ts#L1614-L1615)。
实现层还有防御性校验:目录名必须非空且唯一;网关返回的每个工具名必须能映射回本地 catalog 且不得重复,否则抛错(index.ts#L1617-L1634);sentSchemaTokens <= fullSchemaTokens 不成立时两个计数按 0 处理。
cave.toolSearch(catalog, query, opts?) 是脱离 tools() 句柄的直接变体,契约完全一致(index.ts#L526-L532)。
六、compress():委托式压缩与字节安全 fail-closed
cave.compress(payload, opts?) POST /sdk/v1/compress 并映射 Engine 报告(index.ts#L1650-L1706)。核心语义是 “SDK 委托,不重实现压缩器”;任何一步出问题都走 fail-closed 直通:
- 触发条件:非 2xx、网络异常、响应体不是带字符串
output的报告、tokens_before/tokens_after非严格非负整数、tokensAfter > tokensBefore、或output === payload但计数不一致; - 直通结果:
output为原始输入、ratio: 0、无recoveryHandle、tokenCountBasis: "unavailable"; ratio永远由客户端用校验后的计数重新推导((before - after) / before),不复用服务端字段,避免“乐观”报告;basis恒为"inferred"—— SDK 永不输出verified(那需要 Cloudactive路径背书)。
CompressResult 还包含 contentType、tokensBefore/After、recoveryHandle(恢复字节级原稿的句柄,未存储时缺省)、method(如 "toon"、"elision")、losslessToModel(模型可见输出是否完整保留值)。CompressOptions.contentType 可作为检测器提示,取值 "json" | "toon" | "log" | "code" | "diff" | "search-result" | "text" | "toolschema" 等(index.ts#L43)。
七、上下文打包与可逆 Checkpoint
cave.context.pack(query, items, options) 是仅连接态(connected-only)的有损选择器:POST /sdk/v1/context/pack,把调用方拥有的片段字节发给网关,由它决定“什么进入模型窗口”(缓存最优装配 assemble() 决定“放在哪里”,两者互补而非替代)。它从不写 CCR、不跑在本地 wrap 里,并要求调用方保留 deferredIds 点名的每一条片段以便重供(index.ts#L1708-L1796)。
输入契约(ContextPackItem / ContextPackOptions,index.ts#L78-L107):
- item:
id(稳定、调用方拥有,用于精确报告被省略项)、text、可选tokens(缺省或 0 时由网关计数)、timestamp(RFC 3339,供 recency 信号)、priority(调用方相关性加权)、pin(必须入选;若 pin 项放不下,调用返回诚实的零); - options:
maxTokens(必须 > 0)、reserveTokens(为下一轮响应/工具调用预留)、now(RFC 3339 确定性评分时钟)、recencyHalfLifeMs、recencyWeight、errorBoost。
返回值校验极其严格,任何一条不满足都回退为“全部原始 item + 零推断节省”的直通:选中 id 必须全部存在于请求且无重复;deferredIds 必须与“请求集减选中集、按请求序”逐位相等;tokensUsed + tokensSaved === tokensBefore;deferredCount === deferredIds.length。这保证 ContextPackResult 中的数字自洽,basis 恒为 "inferred"(该选择器从不进入 verified-savings 账本)。
可逆 checkpoint 分两半:
CaveTrace.context.checkpoint(messages, options)→ POST/sdk/v1/checkpoints,网关持久化(Valkey)并返回可逆的source_ref;CaveTrace.context.expand(sourceRef)→ GET/sdk/v1/checkpoints/{ref}/expand,取回存储的{ source_ref, version, messages, checkpoint }。响应缺source_ref或messages数组时抛错——源码注释明确:“不能展开的 checkpoint 是 bug”,可逆性是强制要求(index.ts#L847-L864)。
八、Provider 客户端与 Bedrock 描述符
cave.openai()/anthropic()/gemini()/vertex() 是薄 provider 客户端,全部经网关前缀(/openai/v1、/anthropic、/gemini、/vertex)代理,每个都暴露 .raw fetch 逃生舱(镜像 Python Provider.raw);upstreamKey 用于转发上游凭据(如 Vertex 的 Google OAuth2 access token 会以 Authorization: Bearer … 转发)。
cave.bedrock({region, endpoint?}) 是不发任何网络请求的第一方路由描述符(index.ts#L413-L425):
endpoint缺省为"runtime",网关前缀/bedrock;显式"mantle"返回/bedrock/anthropic;其他值直接抛错;- 返回
{ region, endpoint, gatewayPrefix, instrumented: true, sdkOnly: false },供 AWS SDK 或 agent 包装器自行接线,本身不携带任何 AWS 密钥。
另外 cave.prompts.internalBrevity({style, preserveErrorsVerbatim?, preserveCodeVerbatim?}) 生成输出风格片段(style:"none" 返回空串),镜像 Python cave.prompts.internal_brevity(index.ts#L597-L600)。
九、零依赖 OTel 导出器
cave.exporter({serviceName?}) 返回 OTelExporter(index.ts#L443-L456):
recordSpan(...)把当前 GenAI 字段映射到gen_ai.*属性;SpanOptions涵盖inputTokens/outputTokens/cachedTokens(cached 是 input 的子集,绝不叠加)、costUsd、provider/model/operation/toolName、status("unset"|"ok"|"error")、纳秒级起止时间与自由attributes;export()以 OTLP/JSON POST 到标准/v1/traces,携带 Caveman 标准头(x-cave-api-key/x-cave-agent/x-cave-workflow,由otlpHeaders()组装);legacy 路径/otlp/v1/traces仅保留为服务端兼容;serviceName缺省为Cave的 agent slug(网关用它兜底 agent 标签);span 落库到caveman.spans,全程无需外部 OpenTelemetry 接线。
十、RuntimePolicyClient:本地决策 + Ed25519 签名策略
cave.runtimePolicy({publicKey?, autoRefreshSeconds?, killEnv?}) 返回 RuntimePolicyClient(index.ts#L1068-L1388),是路由面而非节省面——“只做路由,不含任何 savings 词汇”。
refresh():唯一的网络调用(GET /sdk/v1/runtime-policy,标准头去掉 content-type):
- 限流读:响应上限 1 MiB,超限(
content-length预判或流式累计)抛oversized_response并取消读取(index.ts#L873-L912); - 30 秒超时:与 Python
timeout=30镜像,挂死的端点不会挂住调用方; - 先验签、后解析:对 bundle 字符串的精确 UTF-8 字节做 Ed25519 验证(WebCrypto SPKI 导入,index.ts#L1534-L1547);
- TOFU 钉死时机:
publicKey在签名验证成功的瞬间即被钉死——早于 schema/sequence 检查。这样“一个签过名但本客户端因形状拒绝的 bundle”不会打开降级窗口:随后到达的未签名 bundle 会被拒; - 拒绝回退 sequence、拒绝未知
schema_version(当前接受caveman.runtime-policy.v1)、拒绝回退版本; - 任何失败都保留 last-known-good:
refresh()永不抛错,只返回{ok, signed, error?}; autoRefreshSeconds的 tick 若落在 refresh 飞行期间则跳过而不是堆叠(refreshing标志,index.ts#L1085-L1095)。
decide(taskFamily, {unitKey, context, trace}):同步、纯本地、永不抛错。求值链(index.ts#L1268-L1329):本地 kill(kill() 或 killEnv 环境变量,缺省 CAVEMAN_POLICY_KILL,每次 decide 重读,0/false/no/off/空 视为未置位)→ 无 bundle(policy_unavailable)→ bundle kill 旗标 → 无匹配 task_family(no_policy)→ 无效策略(invalid_policy)→ 全 disabled(disabled)→ guard AND 求值(eq/ne/gt/gte/lt/lte/in,类型不符判 false 而非强转,未知算子 fail-closed)→ 匹配多于 1 条即 ambiguous_policy(与服务器“胜层 >1 ⇒ 全弃”语义一致,绝不猜)→ 实验分臂:holdout 切片先切并强制压制到 fallback 路径(“holdout 是抑制,不是更危险的变体”);缺 unitKey 或实验配置非法一律 fallback,不猜臂。
分臂的确定性分数由 policyUnitFraction(...keys) 计算:index.ts#L1509-L1524 逐字节复刻 Go 侧 shared/platform/sampling.Fraction——每个 key 前缀 8 字节大端长度后整体 SHA-256,取前 8 摘要字节为大端 uint64,右移 11 位除以 2^53 得到 [0,1) 分数;该函数被导出以便调用方复现分臂、让跨语言 fixture 逐位钉死这一移植。
decide() 可选地把决策写成一个 span(caveman.policy.decision,属性含 cave.policy.decision/reason/signed/id/version 与实验 id/臂/propensity),sink 是鸭子类型(接受任何带 recordSpan 的对象或 CaveTrace),且所有与 sink 的交互都在 try 内——“遥测永不破坏路由”。state() 返回快照(hasBundle、signed、bundle kill 旗标、killedLocally、policyVersion/sequence),close() 释放后台定时器(镜像 Python close())。
十一、RetryLoopBreaker 与预留的 JobsClient
cave.retryLoopBreaker(threshold=3) 返回 RetryLoopBreaker(index.ts#L664-L709):record(name, args) 以“工具名 + 键排序 JSON 序列化参数”作为签名,同一签名连续重复超过阈值即在第 threshold+1 次抛出 RetryLoopError(携带签名、重复次数与阈值);任何不同的调用重置连击;guard(name, args, fn) 先记录(可能抛错)再执行 fn;reset() 在新任务开始时清零。
cave.jobs 是预留的异步作业面:submit/status/cancel/wait/submitAndWait 全部在任何网络 I/O 之前本地失败,抛 AsyncJobsUnavailableError(code cave_async_jobs_unavailable)。源码注释解释了原因:在持久加密请求存储、凭据托管与排空 worker 就位之前,该表面拒绝制造“假 queued/completed”的假象。
十二、Wire 约定、Gotchas 与验证路径
AGENTS.md 的 Conventions 与 Gotchas 是该 SDK 的行为红线,逐条对应源码事实:
- byte-safe:SDK 把请求体原样发往网关,绝不重写;
compress()是唯一产出更小字节的路径,且它委托 Engine,任何异常直通原始输入(index.ts#L1651-L1660); - 命名映射:到网关的请求体键是
snake_case(如input_schema、always_load、session_id、max_tools),响应在ToolSearchResult等类型上映射为camelCase; x-cave-workflow永不省略:默认值链为defaultWorkflow ?? "unlabeled-workflow"(见 index.ts#L392、index.ts#L1599);- 延迟工具会话交接三要素:请求
session_id、结果sessionId、provider 头x-cave-tool-session;改动需同步 sdk-python 与 parity fixture; - context packing 仅连接态且刻意有损:它选择“什么进窗口”,缓存最优装配决定“放哪里”;
- mirror sdk-python:每个字段/方法在两个 SDK 中都存在,由共享 parity 套件强制——分歧即 CI 失败;“改一个 SDK 就要改两个,还要改 fixture”;
- 发布名
@caveman-ai/sdk,workspace 名在 npm 重定向计划落地前保持不变; reductionPct保留一位小数;savedTokens为派生值而非网关字段。
验证路径固定为三步:
# 1. 构建(dist/ 是运行时测试的导入源)
pnpm build
# 2. 类型级断言(只编译,不执行)
pnpm test:types
# 3. 运行时测试(against dist)
pnpm test:node
十三、小结
Caveman 的 TypeScript SDK 是一个“把网关能力翻译成类型安全本地 API”的薄层:所有重活(压缩、排序、策略下发、checkpoint 存储)都在网关/Engine 侧,SDK 只负责三件事——忠实透传字节、fail-closed 地降级、以及让本地决策(策略、断点装配、重试打断)永不阻塞、永不猜测。跨语言 parity fixture 与“basis 恒为 inferred”的诚实性纪律,使它成为在多 SDK 表面共享同一套 wire 契约时,一个可直接对照 packages/sdk/ 下 Python 实现逐条核对的工程样本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00