09 · LLM 服务、协议适配、重试和可重放上下文¶
DSH 把“请求一个模型”和“驱动一个 Agent”分开:ctx.llm 提供可注册 adapter 的模型 seam,agent-loop 管理 Session、turn、step 与工具。模型协议的差异归 adapter,上下文 admission 与日志归 loop,失败恢复通过插件事件组合。本章固定 main da00f7f;发布实验使用 rc.2,不要把源码默认模型目录当成今天所有账户可用模型的承诺。
9.1 先读四种类型,而不是先看 fetch¶
LlmCallConfig 是 request envelope 的调用选择:provider/model、reasoning/maxTokens 等;GenerateOptions 再加入 messages、tool schemas/history、signal 和辅助用途。LlmResolvedModelInfo 描述一个真实 route 的上下文窗、输入 modalities、systemPromptUpdate/toolUpdate 能力。StreamChunk 是 provider-neutral 增量词汇;BlockAssembler 把它们收敛为 content、usage 和 finish。Service Definition 不依赖“OpenAI JSON 大概长这样”,而要求 adapter完成具体协议转换。
registerAdapter(providers, adapter) 一个实例可以拥有多条 route;重复 provider、空 provider、无 route会拒绝。注册 handle 是 effect disposer,并有 replace(nextRoutes):先验证全候选集合,再提交,不在验证中半写 registry;已 dispose 的 handle不能重新占 route。工具/Agent不需要知道是哪个 library实现 route。源码:index.ts · registerAdapter(providers:。
9.2 Prepared Call 消除“元数据是旧模型,发请求是新模型”的竞态¶
如果先查询模型支持图片,再隔一段时间以名字重新 lookup adapter,期间 HMR/配置变化会让最终请求落到另一代。LlmRuntime.prepareCall() 先捕获 registration,再调用 adapter.prepareCall 得到这一代的 model metadata 与绑定 stream。resolveCallWithInfo() 把 adapter defaults补入调用 config,再 detached clone/deepFreeze config、context、modalities、update能力和 retryPolicy。返回值的 stream() 只能调用一次,而且 options中的 call-config必须与捕获 config相等;重复 dispatch或中途改路由会抛 INVALID_PREPARED_CALL。源码:index.ts · async prepareCall(config:。
调用者因此必须按顺序做四件事:准备 route → 按这个 route 的能力处理 prompt/surface → 将 admitted system/users/header/context记录日志 → derive/freeze request并使用该 prepared stream。不能在 llm/stream listener里偷偷改 messages:loop-built request有过程内 marker且深冻结,模型可见内容必须由日志解释;手工辅助调用没有 loop marker,但调用者仍拥有一致性和不可变期间的责任。
llm/stream 是 waterfall:调用 next()到捕获 adapter,也可以 yield自己的 chunk截断下游,用于 replay/observability/routing设计;“监听 stream”不等于有权绕过 durable request admission去换内容。副作用 provider替换应在 documented Agent request seam解决,并记录 envelope。
9.3 官方 DeepSeek route 不只是 OpenAI chat/completions¶
llm-deepseek 拆出 protocol实现,llm-deepseek-api-key 拆出认证/discovery,llm-deepseek-account接账户凭证。API-key插件的 route id是 deepseek-official。apiKeyEnv 是 credential reference,默认 DEEPSEEK_API_KEY;有 credentials service时先从那里解引用,无 seam才读 frozen launch environment。缺凭证报 MISSING_CREDENTIAL,非法HTTP header key报告credential reference,不把secret打印到诊断。源码:index.ts · const PROVIDER、index.ts · const resolveApiKey。
配置暴露 baseURL、thinking、reasoningEffort、maxTokens、model目录、流 idle timeout、image/file bounds与 retryPolicy。其中volatile配置在 plainOptions()读出 请求级完整快照;DeepSeekAdapter.prepareCall把 connection与model metadata绑定同一代。没有显式 baseURL时,trusted launch层中的 DEEPSEEK_BASE_URL再到公共默认 https://api.deepseek.com/anthropic;发送 ${messagesApiRoot(baseURL)}/messages。这是 Messages-style内容与SSE,不应拿只会返回Chat Completions delta的假服务器冒充通过。源码:config.ts · export const PUBLIC_BASE_URL、adapter.ts · override prepareCall。
实际 request连续流程如下。
- 检查 signal;
prepareImages()处理模型能力、attachment access和image版本。 - 获取这份 connection匹配的 auth;建立 RequestFiles上下文。
- 先尝试准备 Files API ids。受控文件解析失败可转 inline image重做 序列化准备;不是重做tool side effect。
serialize()将 Harness messages/reasoning/tool history转为 Messages wire,必要时报告 replay降级。prepareRequestExtensions()组装 DeepSeek wire扩展;辅助扩展失败有专用 omission diagnostics,不应误说所有 extension失败必定模型请求失败。- fetch 使用
redirect: 'error',带 attribution、版本、必要beta与匿名user/session id。先处理HTTP failure,再检查SSE和终止语义。 parseSse()用 eventsource-parser做完整帧边界;heartbeat/comment与frame pulse idle watchdog;JSON损坏或event type不匹配报 MALFORMED_RESPONSE。- translator收集 text/thinking/tool-use等增量。消费停止时
finallyabort consumer并return内部iterator,避免reader与网络工作遗留。
源码:adapter.ts · private async * request、sse.ts · export async function* parseSse。
streamIdleTimeoutMs计流读取活动的空闲,不是Agent总任务时长,也不是token budget。默认protocol值为300000 ms;sdk-minimal bundle显式覆盖172800000 ms。全局 maxTokens默认256000、contextWindow fallback1000000,model-specific/request overrides仍优先;对自己小context模型必须显式配置,不可照抄这些超大默认。模型目录中 deepseek-flash的display name为DeepSeek-V41-Flash,deepseek-v4-pro为另一entry;这是本commit的advisory directory,不表示发布实验必须连真实收费服务。源码:defaults.ts · DEFAULT_STREAM_IDLE_TIMEOUT_MS、models.ts · export const DEFAULT_MODELS。
9.4 pi-ai 是协议库插件,不是借用 Pi 的 Agent 循环¶
llm-pi-ai把pi-ai现成provider catalog、auth、model目录和stream适配到DSH类型。它持有配置provider profiles,按 raw snapshot身份memoize解析;profile更新验证serviceability。registerConfigurableProviders()让Settings/Models消费者知道route归哪个settings namespace;discovery可以保留catalog error诊断,而不是把配置删掉。
关键认证规则:profile一旦明确apiKeyEnv,缺这个key必须失败。 不允许把undefined交给pi-ai,让library自行捡另一个 ambient OPENAI_API_KEY而意外给别的租户计费。只有没有指定credential ref时才允许provider-native auth discovery/OAuth路径。OAuth状态与connection generation也要按plugin生命周期持有。源码:index.ts · const resolveApiKey。
context.ts/replay.ts负责Harness content到pi-ai transcript,包括tool updates、reasoning和跨provider历史的退化策略;stream.ts负责增量词汇;adapter.ts绑定route dispatch。你已有Pi教程,可复用对pi-ai provider streaming的理解,但DSH的session log、request freeze、tool pipeline、compaction transaction仍是DSH自有实现。不要把pi-ai支持某provider等同于当前profile已配置可用、当前模型具备image或in-history tool updates。
9.5 错误词汇、重试归属和durable backoff¶
LlmError/LlmFailure把transport/HTTP事实转换为provider-neutral code,保留合法status、Retry-After、opaque requestId、offloadImages等结构化信息。重试配置由provider注册拥有;llm-retry config为空,写 retryPolicy到retry插件会被明确拒绝。源码:index.ts · function validateConfig。
normal policy只重试声明retryableCodes,有maxRetries;always policy允许无界重试,但仍受外部signal、plugin lifetime和下游具体恢复决策约束。每个provider+policyKey有durable retry projection,step/start或turn/end重置;同一步恢复时不会用新进程内计数忘掉已消耗预算。delay先算指数退避与jitter,再处理providerRetryAfterMs:合法且≤maxDelay直接采用;normal遇到超过上限的Retry-After会委托下游,不是强行截短后重试;always在这种情况下回退本地delay。
日志在wait前记录计划、wait后记录retry-started,意味着看到retry event不能断言下一次HTTP已发送。unload先dispose listener再abort并drain active waits。context overflow/image offload用专门修复插件,不能只靠网络重试增加同样的大请求。源码:index.ts · async function backoff、index.ts · if (failure.providerRetryAfterMs。
更重要的是模型request重试与tool重试不同:一次失败模型attempt不会让已经提交的tool重新执行,program/workflow/browser动作也不自动回滚。你自己的写入/支付工具需要另建幂等键与外部状态核验。
9.6 TokenMeter 的估计、计费与上下文压力不是同一数字¶
token-meter/estimate.ts的基础估计是text每4个JS字符约1token,加role/block/schema framing;未知content用结构JSON估计;图片是reference结构估计,真实request image price由route-owned pricing给出。它不是真实模型tokenizer,中文、代码、特殊字符不会保证固定密度。源码:estimate.ts · const CHARS_PER_TOKEN。
measure(session, requestHeader?)基于当前surface positional nodes逐节点计价,结合latest成功call的usage anchor。当前anchor按同一route重新计价;provider usage只在能够安全作为基线时采用,否则用估计基线。surface replace/prune/append产生delta,tool schemas也属于envelope价格。totalTokens用于request pressure;辅助summary有自己的usage,UI的turn usage与请求上下文压力不是同一账本。源码:index.ts · measure(session:。
自己做监控至少分别展示:request估计输入压力、provider-reported input/output/cache usage、辅助compaction usage、实际费用依据(若provider没有返回价格则不能凭目录乱算)、request/step/turn次数和retry消耗。不要把估计token当费用结算凭据。
9.7 BasicCompaction:压缩surface,而不抹掉原事件¶
BasicCompaction是可替换CompactionEngine。automatic监听 agent/pre-step做pressure检查,在 agent/request-error专门处理CONTEXT_WINDOW_EXCEEDED。原始session事件保留,通过新的shadow/replacement事件改变current model surface,重放仍能解释“当时被替换的是哪些seq”。默认thresholdRatio0.8、retainRatio0.16、headroom65536、maxTokens默认headroom、summary retries1、overflow retries1、auto true。modelPolicies按精确provider/model局部覆盖。源码:config.ts · export function resolveConfig。
设上下文窗W、当前request保留输出M、压缩headroomH、thresholdRatio r,压力阈值是 floor(min(W*r, W-M-H));retention默认 floor((W-M)*retainRatio),不是 W*retainRatio。必须让retention小于threshold,且输出/余量不能吃光window;小模型用默认65536很可能不合适,按route配置比“压缩失败后再试试”可靠。源码:config.ts · export function resolveCompactSpec。
下面两组是按源码公式计算的教学例子,第二组是自定义小模型配置,不是发布版默认。它说明 maxTokens 与 headroomTokens 都会减少可留给输入的压力空间。
| W | M | H | r | 触发阈值 | retainRatio=0.16 的最近尾部预算 |
|---|---|---|---|---|---|
| 1000000 | 256000 | 65536 | 0.8 | 678464 | 119040 |
| 32768 | 4096 | 2048 | 0.8 | 26214 | 4587 |
selectCompactableRange()对token measurement与session.surface逐位置验证;system head在node0则不压入span,倒序累积最近tail到retention预算,再把cut回退到tool pairing平衡位置。不能切断assistant tool-call/result关系,也不能在仍open的step随意结束span。“保留最近若干事件”如果不用surface positions,会把旧shadowed事件或auxiliary log混进去。源码:region.ts · export function selectCompactableRange。
compactSurfaceRegion()在validation与start append之间不await,opening marker就是durable锁;summary异步之后还要稳定性核验。manual idle compaction要求没有open turn,whole-surface稳定且可执行flush checkpoint;automatic拥有current-turn,按selected-span规则确认。summary/changed/commit/persistence失败给不同ManualCompactionError类别;end append失败会留下可检测unmatched start,不能默默宣称压缩成功。源码:region.ts · export async function compactSurfaceRegion。
summarizer复用当时route、system、tools与conversation prefix,把compaction指令作为最后user message;这样有机会复用provider暖前缀,不保证实际cache hit。辅助调用purpose为compaction,保留rawOutput与usage,只接受有用text summary,frame成checkpoint user message。默认summary不是另起一个完全不同system prompt。源码:summarizer.ts · export async function summarizeWithLlm。
pressure压缩失败可以log warning并继续原turn;overflow失败若先前无模型prune已经提交surface progress,仍允许受限retry,否则保留原request error。成功assistant message重置overflow recovery序列。看到warning不必误判原request一定已停,但看到surface progress也不代表summary阶段成功。
9.8 Pruner、图片卸载和附件上传的不同职责¶
ToolResultPruner按Unicode code points计文本,超过threshold时保留head/tail和中间marker,非text块保留;它修改durable surface,不用LLM,不是简单截UI preview。它不会保证保留grapheme cluster,但避免切开surrogate pair。源码:index.ts · pruneContent(blocks:。
ImageOffload针对provider提出的IMAGE_OFFLOAD_REQUIRED,选最旧retained occurrences追加 image/offload并通过message projection在后来请求送placeholder。它是surface repair,不耗network retry预算、不写llm/retry;summary-error也有相同修复路径。卸载不是删除attachments,更不是撤销图像已上传到provider。Files API quota cleanup只处理Harness-owned indexed files,生命周期、上传refresh与session model context要分别理解。源码:index.ts · export function apply。
9.9 Instructions、skills、time和reference怎样成为可重放事实¶
AgentInstructions 在pre-step拼工作区baseline,沿project root和配置的candidate files读取,并为subdirectory文件touch做增量changes。source记录baseline身份、版本/摘要和scope;read/write/edit tool的file_path可触发后续准备,execution/step ancestry控制何时注入,避免异步observer在错误的turn把上下文插进去。所有有效context最终是typed user/message,不是隐藏修改system字符串。maxBytes、maxSourceBytes、候选文件、root marker影响结果;不用此插件则不自动取得同样指令。源码:index.ts · export function apply。
Skills 有separate registry/provider/consumer。filesystem provider 的目录 rank 顺序为 project .dsh、project .agents、custom、user .dsh、user .agents,bundled 另有 rank。rank 越小越优先,但它只在同一 scope layer 内比较;跨层同名 skill 先由离调用者最近的 layer 获胜,再比较该层候选。源码:SkillRegistry 的层级合并。扫描可发现directory SKILL.md和flat markdown,解析frontmatter。catalog是摘要,tool-skill显式加载body;invocation policy与支持assets不等于沙箱授权。默认watch true,支持polling、stability和maxProjects;agent writes的fs/observed可刷新目录。要做可重复产品,显式 includeDefaultRoots: false + customSkillDirs,防止机器个人skills意外进入Agent。源码:index.ts · const PROJECT_DSH_RANK、index.ts · export interface Config。
TimeContext opt-in pre-step按 refreshIntervalMs(默认 600000,即 10 分钟)产durable source-attributed clock message;step1和laterstep elapsed reference不同。读取当前turn中的browser zone,有唯一可信zone则使用,否则fallback配置/系统zone。时间采样放历史而非每个token重写system,可保前缀稳定。clock读数不是调度工具;调度consumer在main新preset中声明,见调度章。源码:index.ts · export function apply。
FileReference 是文件引用语法/解析,local实现负责查找/读取候选;SessionReference 精确读其他session的readonly snapshot,限制引用数、context fraction/bytes,按引用顺序将prepared snapshot放在direct user消息后。prompt清楚标记引用session是untrusted data;不恢复另一个Agent的live handle,不复制其权限,不执行那份旧指令。保持 startsRequestSeries 等decision metadata很关键:wrapper应spread decision而不是重建只有messages的对象。源码:index.ts · private async prepareDirectMessages。
9.10 自己的Agent应该配置什么¶
领域助手需要route-specific context window/output cap、normal bounded retry、明确的skills roots、受限instruction candidates和model-visible来源。选择小模型时先重新算compaction阈值,离线mock用小text response验证真实protocol,不把token估计准确性/真实cache效果写成已经验证。
主分支与rc.2的关键差异是删掉大量 /invariant companions、profile/HMR解析变更、preset新增time/schedule rows;本章的DeepSeek协议、pi-ai adapter、PTC runtime等核心路径在两commit间并非全盘新实现。版本附录给出完整逐包变更范围;实验必须使用rc.2的exports,不随手复制main README到发布API。