ByteNoteByteNote
DeepSeek Harness Typert 调用契约解析
字

字节笔记本

2026年10月6日 · 约 10 分钟读完

DeepSeek Harness Typert 调用契约解析

API中转
¥120

DeepSeek Harness 是 DeepSeek 在 GitHub 上开源的 agent 框架,客户端与 Host 进程之间的远程方法调用由一套名为 Typert 的协议支撑。项目文档里有一页专门登记这套协议的公共契约:这些类型由生成的 Remote 产物、Host Gateway 与消费方 API 装配三方共用,字面定义分布在 dsh-typert-protocol 与 dsh-api-gateway 两个包里。架构文档解释设计动机,这一页则回答线上到底长什么样。本文把它拆成五块:lookup 声明、调用 descriptor、运行时注册表、网关入口与消费方挂载面。

Typert 远程调用链路:从 $mount 挂载到 Host 方法执行

两个空 map:lookup 与上下文声明

业务对象包通过声明合并扩展两个空接口:TypertLookupMap 与 TypertContextMap。前者的每个条目把一种 Host 对象类型关联到它的 wire identity,比如把内存里的 Agent 对象映射为网络上的 agentId 字段;后者把一种作用域上下文类别关联到 wire identity。生成的 descriptor 引用这些 key,活对象的解析行为则由运行时提供方补上。

每条 lookup 声明是一个 TypertLookupDefinition,五个字段分别是:合并声明的 lookup key、SRC 弱解析器识别的源参数名 parameter、替换 Host 对象参数的 wire 字段,以及供严格生成使用的 Host 与 wire 两侧规范类型 symbol。

值得注意的细节是:lookup 的 resolver 卸载后,注册表仍会保留它的 wire 声明。因此 SRC 发现流程会继续把这个参数归类为 lookup,并因不可用而失败,而不是把 wire 值错当成普通业务对象接受。声明与实现分离之后,参数该怎么传的约定不会随提供方的卸载一起消失。

调用 descriptor:本地反射,不是 wire 消息

InvocationDescriptor 描述一个导出方法的完整调用形态,但它只存在于本地,是反射信息而非 wire 消息。Host 与消费方两侧的构建各自生成彼此对应的 descriptor,真正的网络请求只发送 endpoint 与具名 args,descriptor 本身从不上网。

每个参数由 InvocationParameterDescriptor 刻画:源码参数名 name、wire args 里的必填键 wire、值来源 source(json 或 lookup),以及边界 codec。缺省的 wire 字段只有在签名显式声明了 T | undefined 时才会解码成 undefined,这个语义由 acceptsUndefined 标记。

ts
type TypertCodec =
  | { readonly mode: 'strict'; readonly typeSymbol: string; readonly schema: TypertSchema }
  | { readonly mode: 'src-json' }

codec 分两档:strict 模式携带生成期产出的类型 symbol 与 schema,做结构化校验;src-json 模式面向没有生成 schema 的回退场景,只强制值是 JSON 安全的,不尝试恢复结构类型。

方法取消也不占用参数列表。cancellation 声明把 signal 标记为保留的最后一个 Host 方法参数,由传输层在业务参数之后注入,绝不进入 wire args。此外 descriptor 还记录:wire namespace(默认取服务 key)、方法别名 implementation(导出名只是别名时指向真实服务成员)、接收者选择模式 invocation(直接调用,或先按声明解析作用域上下文再取服务)、可选的 scope 投影(把一个 direct lookup 参数替换为消费方 Context 的 identity),以及仅用于诊断的 sourceLocation。

ctx.typert:一张注册表,四个分区

运行时的 ctx.typert 把四类东西分开保存:当前环境的 descriptor(local)、显式选择的 Remote 贡献(remotes)、lookup 提供方(lookups)与作用域上下文提供方(contexts)。lookup 提供方拥有稳定的 wire 声明和默认 resolver;Host 组合可以为同一个 key 配置 effect 作用域的同步或异步 resolver,配置卸载后自动恢复默认策略。所有注册都是 Cordis 持有的 effect,返回可等待的 disposer。

Typert 失败分流与注册表四分区

从生成的 API 目录看,注册表的常用方法包括:register 以整批原子方式注册一个贡献,包身份、schema、调用 id 或 endpoint 任何一项重复都会整批拒绝;get、resolve、list 按"包名#名称"格式的全局 key 存取 schema 记录;getPackage 与 listPackages 查询某个包面的生成期反射;toJSONSchema 把活的 Zod schema 投影成 JSON Schema 文档,每次现算、不做缓存。消费方一侧,生成的声明会把 direct namespace 合并进 TypertClientRemote 继承的 namespace map。

Host Gateway:一个 invoke,17 类错误码

传输层 Connection 先解码 carrier envelope,再调用 ctx.typertGateway。入口参数 InvokeRemoteRequest 只有四个字段:descriptor 选定的 namespace、导出方法名 method、具名 wire 值 args(字段必须与 descriptor 精确一致),以及可选的取消 signal。

ts
interface InvokeRemoteRequest {
  readonly namespace: string
  readonly method: string
  readonly args: Readonly<Record<string, unknown>>
  readonly signal?: AbortSignal
}

错误处理分三条路。第一,基础设施与边界失败,比如 endpoint 含糊、签名不合法、结果校验不过,发生在业务执行前后,使用网关进程内的错误分类体系,一共 17 类,从 ambiguous-endpoint、lookup-not-found 一直到 service-unavailable、signature-invalid。第二,业务方法抛出的普通异常由 RPC 适配器归并,统一折叠成传输层的 internal 错误码。第三,经 lookup 策略携带的既有 RPC error(TypertLookupFailure)原样返回,错误身份保持不变。调用方由此能区分没进业务逻辑就失败了和业务代码自己抛了错这两种情形。

消费方 Remote:挂载、订阅与派发

客户端侧的 ctx.remote 只暴露由已导入 /remote 产物贡献的 namespace。$mount() 把生成的 descriptor 与具体方法作为一项由 fiber 持有的操作统一注册,namespace 服务与具体方法全部就绪后才交出 disposer。每个 namespace 都是一个可追踪的 remote.<namespace> Cordis 子服务,生命周期覆盖它挂载的所有方法;整个消费方看不见 JavaScript Proxy,也看不见 Host 业务服务类型。

事件转发走 $on 与 $dispatch 一对原语。$on 订阅一个 Host 转发事件,投递是单向的、按注册顺序进行,某个监听器抛异常不影响其余监听器,返回调用方 fiber 持有的退订函数。$dispatch 由持有 Host 帧接收器的 carrier 调用,把解码后的帧交给订阅表;event 在这里只是普通字符串,因为已经站在 wire 边界上,名字就是 Host 装配允许清单选中的那个,没人订阅的事件被静默丢弃。另外,统一 API 根接口 ctx.apiProxy 上还挂着一个 respond 方法,作为服务端请求的响应入口:消费方响应携带 rpcId,返回传输回执。

这套契约的价值

整层契约把跨进程调用的每个环节都钉在类型上:参数如何上 wire 由 descriptor 说了算,值是否可信由 codec 分档把关,提供方卸载后约定仍然有效,失败时三条去路各有归宿。对研究 agent 框架内部设计的人来说,把公共契约原文完整公开的做法并不多见;想自己实现类似 RPC 层的开发者,也可以直接借鉴它在声明合并、带外取消与错误分类上的处理。

相关文章

分享: