DSH / Atlas
2026-07-04implementedsimplificationarchived 2026-07-26

Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path

移除 `GenerateOptions.prefill` 与 `ToolSchema.strict`——无端到端可用路径的请求旋钮

Two request-contract knobs rode the whole request pipeline, yet neither could do anything: - **`prefill`** (`packages/llm/llm/src/types.ts`) had no production setter — the loop assembles `model`/`system`/`tools`/`messages` plus `sessionId`/`signal`, and the compaction backend adds only `maxTokens` — and BOTH adapters rejected it: `packages/llm/llm-deepseek/src/serialize.ts` and `packages/llm/llm-pi-ai/src/adapter.ts`

English

Problem

Two request-contract knobs rode the whole request pipeline, yet neither could do anything:

  • prefill (packages/llm/llm/src/types.ts) had no production setter — the loop assembles model/system/tools/messages plus sessionId/signal, and the compaction backend adds only maxTokens — and BOTH adapters rejected it: packages/llm/llm-deepseek/src/serialize.ts and packages/llm/llm-pi-ai/src/adapter.ts each threw LlmError('UNSUPPORTED') on a non-undefined prefill. The field's entire observable behavior was two throws, each pinned by one adapter test. DeepSeek's chat-prefix completion is a Beta feature on a base URL neither adapter targets.
  • strict (ToolSchema, same file) was threaded through DefineToolOptions/defineTool (packages/core/tools/src/schema.ts), the registry's schemas() allowlist (packages/core/tools/src/index.ts), the deepseek wire mapping (packages/llm/llm-deepseek/src/serialize.ts, whose wire-type note recorded that strict mode requires the /beta base URL the adapter does not use), a per-tool payload-patching pass in packages/llm/llm-pi-ai/src/adapter.ts, and a conditional Strict: row in the tool-catalog renderer (scripts/gen-tool-catalog.ts). No shipped tool set it — rg across every tool-* package src and examples/ found zero strict: producers; the only setters were dsh-tools unit tests.

Both knobs were adapter-symmetric, so removal shed them from both twins together — the twin-adapter design is untouched.

Decision

  • prefill is removed from GenerateOptions, along with both adapters' UNSUPPORTED guards, the tests pinning the throws, the paste line in core.md, and the adapter README rows documenting the rejection. The cookbook's UNSUPPORTED guidance (adding-an-llm-adapter.md) states the rule generically — a GenerateOptions field your provider cannot honor throws LlmError(..., 'UNSUPPORTED') — instead of using prefill as the example. The content-block vocabulary Agent Note's consequences record prefill as producer-gated rather than as having a home, per implemented/AGENTS.md.
  • strict is removed from ToolSchema, DefineToolOptions, defineTool, the schemas() allowlist, the deepseek serializer branch and its wire-type field, and the tool-catalog renderer's Strict: row. The pi-ai payload fixup is simplified to the unconditional scrub of pi-ai's own per-tool strict default (pi-ai stamps strict: false on every serialized tool; the hand-rolled twin sends no such field, so the scrub survives for wire parity, pinned by its serializer test). The setter tests and the core.md paste line are gone; both GenerateOptions and ToolSchema keep their rows in scripts/type-equiv.manifest.json, since each type survives minus a field.

This Agent Note deliberately does NOT touch temperature, stop, or maxTokens: those are honored end-to-end by both adapters and are the natural first targets of a request-mutating hook plugin on agent/request.

Alternatives considered

Why not keep them?

"An explicit UNSUPPORTED throw is honest contract behavior" — but a knob whose only implementation across both twins is rejection promises nothing, and deleting it upgrades the failure mode: an accidental setter becomes a compile error instead of a runtime throw. "Strict schema adherence is an officially documented provider feature with complete plumbing" — but a knob is not product surface until a shipped tool sets it AND an endpoint honors it; today neither is true. Each returns with its first real producer: prefill together with an adapter that implements chat-prefix completion (and a stated policy for adapters that do not), strict together with a tool that wants it and a beta-endpoint story.

Verification

rg prefill returns only Agent Note records (this one and the content-block vocabulary Agent Note's producer-gated consequence); a tool-schema-scoped rg strict returns only this Agent Note, the surviving pi-ai scrub, and unrelated prose such as strictEqual. Both adapters' contract tests pass without the guards, and the pi-ai fixup still scrubs the library's strict default — wire parity pinned by its serializer tests.

Consequences

The shipped hook bridges set no request fields at all, and a request-mutating plugin (an agent/request waterfall listener) reaches for temperature/stop (kept, working), not a field adapters reject. If chat-prefix completion or strict mode become product features, the re-add lands with the adapter/endpoint work, where the contract can say what actually happens rather than "everyone throws".

中文

问题

两个请求契约旋钮贯穿了整条请求流水线,却都无法产生任何效果:

  • prefillpackages/llm/llm/src/types.ts)没有生产级的 setter:agent loop(智能体循环)组装的是 model/system/tools/messagessessionId/signal,上下文压缩(context compaction)后端只追加 maxTokens;而且两个适配器都拒绝它:packages/llm/llm-deepseek/src/serialize.tspackages/llm/llm-pi-ai/src/adapter.ts 各自在 prefill 非 undefined 时抛出 LlmError('UNSUPPORTED')。该字段的全部可观测行为就是两个 throw,各由一条适配器测试固定。DeepSeek 的 chat-prefix completion 是一个 Beta 功能,运行在两个适配器都未指向的 base URL 上。
  • strictToolSchema,同一文件)穿过了 DefineToolOptions/defineToolpackages/core/tools/src/schema.ts)、注册表的 schemas() 允许列表(packages/core/tools/src/index.ts)、deepseek 协议格式(wire format)映射(packages/llm/llm-deepseek/src/serialize.ts,其 wire-type 注释记录了 strict 模式需要适配器未使用的 /beta base URL)、packages/llm/llm-pi-ai/src/adapter.ts 中的逐工具 payload 修补逻辑,以及 tool-catalog 渲染器(scripts/gen-tool-catalog.ts)中的条件 Strict: 行。没有任何已发布的工具设置过它——在所有 tool-* 包的 src 和 examples/ 中执行 rg 搜索,strict: 的生产者为零;唯一的 setter 出现在 dsh-tools 单元测试中。

两个旋钮在适配器间是对称的,因此移除操作将它们从两个孪生适配器中一并剥离——孪生适配器设计不受影响。

决策

  • GenerateOptions 中移除 prefill,同时移除两个适配器的 UNSUPPORTED 守卫、固定抛错行为的测试、core.md 中的粘贴行,以及记录该拒绝行为的适配器 README 表格行。实操手册中的 UNSUPPORTED 指导(adding-an-llm-adapter.md)改为通用表述规则——提供方无法遵守的 GenerateOptions 字段应抛出 LlmError(..., 'UNSUPPORTED')——而不再以 prefill 为例。内容块词汇 Agent Note(agent 决策记录)的后果按照 implemented/AGENTS.md,将 prefill 记录为由生产者门控,而不是已有归属。
  • ToolSchemaDefineToolOptionsdefineToolschemas() 允许列表、deepseek 序列化分支及其 wire-type 字段,以及工具目录渲染器的 Strict: 行中移除 strict。pi-ai 的 payload 修补逻辑简化为对 pi-ai 自身逐工具 strict 默认值的无条件清除(pi-ai 在每个序列化的工具上打 strict: false;手写的孪生适配器不发送此字段,因此清除逻辑为保持协议格式对等而保留,由其序列化器测试固定)。setter 测试和 core.md 粘贴行已移除;GenerateOptionsToolSchemascripts/type-equiv.manifest.json 中保留各自的行,因为两个类型只是少了一个字段,本身仍然存在。

本 Agent Note 刻意不触及 temperaturestopmaxTokens:两个适配器都会端到端遵守它们,而且它们自然是 agent/request 上修改请求的钩子插件首批目标。

曾考虑的替代方案

为什么不保留?

「显式的 UNSUPPORTED throw 是诚实的契约行为」——但一个在两个孪生适配器中唯一的实现就是拒绝的旋钮,什么也没承诺;删除它反而升级了失败模式:意外的 setter 变成编译错误而非运行时 throw。「Strict schema 遵循是官方文档记载的提供方功能,且管道完整」——但一个旋钮在有已发布的工具设置它并且有端点兑现它之前,不构成产品表面;今天两者都不成立。它们各自随首个真实 producer 回归:prefill 随实现了 chat-prefix completion 的适配器(以及对不支持该功能的适配器的明确策略)一起回来;strict 随需要它的工具和 beta 端点方案一起回来。

验证

rg prefill 只返回 Agent Note 记录(本文及内容块词汇 Agent Note中由生产者门控的后果);限定在工具 schema 范围内的 rg strict 只返回本 Agent Note、保留下来的 pi-ai 清理逻辑,以及 strictEqual 等无关正文。两个适配器的契约测试都能在没有守卫的情况下通过,pi-ai 修正仍会清理库的 strict 默认值——其 serializer 测试固定了线协议一致性。

后果

已发布的钩子桥接不设置任何请求字段,而请求变更插件(agent/request waterfall(瀑布式事件)监听器)使用的是 temperature/stop(保留且可用),而非适配器拒绝的字段。如果 chat-prefix completion 或 strict 模式成为产品功能,重新添加将随适配器/端点工作一起落地,届时契约能说明实际发生了什么,而不是「所有人都 throw」。