Define the in-file RFC contract in docs/rfc/README.md § The file format: the header block (`# RFC: <title>` plus a dateless Status enum cross-checked against the lifecycle folder), the per-lifecycle body skeleton (a Problem opener everywhere; Proposal/Alternatives considered/ Acceptance criteria/Risks in proposed/; present-tense Decision/ Consequences with proposal-era headings banned in implemented/; the frozen proposal shape in rejected/), and a mandatory Alternatives considered section with a date-fenced grandfather comment for pre-format RFCs whose alternatives are not reconstructible from the record. Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the enum, 29 Context openers become Problem, the 39 legacy-format XXX debt markers are resolved and banned from reappearing, proposal-era sections in implemented RFCs are rewritten to shipped reality (including the web/fs/subagent seam RFCs' migration plans and test checklists, closing the doc-tiers deferred-work item on the web seam), every RFC gains an Alternatives considered section or the grandfather comment, and the bilingual pair is re-mirrored and re-recorded. Move the generated index tables out of README.md into a fully generated docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and verify-rfc-classification checks its freshness and rejects index-shaped rows in the curated README — which makes room for the format contract to live in the README front door instead of a separate FORMAT.md. The decision record, and the first RFC written in the new format, is docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
4.9 KiB
RFC: Drop GenerateOptions.prefill and ToolSchema.strict — request knobs with no working end-to-end path
Status: implemented
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 assemblesmodel/system/tools/messagesplussessionId/signal, and the compaction backend adds onlymaxTokens— and BOTH adapters rejected it:packages/llm/llm-deepseek/src/serialize.tsandpackages/llm/llm-pi-ai/src/adapter.tseach threwLlmError('UNSUPPORTED')on a non-undefinedprefill. 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 throughDefineToolOptions/defineTool(packages/core/tools/src/schema.ts), the registry'sschemas()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/betabase URL the adapter does not use), a per-tool payload-patching pass inpackages/llm/llm-pi-ai/src/adapter.ts, and a conditionalStrict:row in the tool-catalog renderer (scripts/gen-tool-catalog.ts). No shipped tool set it —rgacross everytool-*package src andexamples/found zerostrict: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
prefillis removed fromGenerateOptions, 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 — aGenerateOptionsfield your provider cannot honor throwsLlmError(..., 'UNSUPPORTED')— instead of using prefill as the example. The content-block vocabulary RFC's consequences record prefill as producer-gated rather than as having a home, per implemented/AGENTS.md.strictis removed fromToolSchema,DefineToolOptions,defineTool, theschemas()allowlist, the deepseek serializer branch and its wire-type field, and the tool-catalog renderer'sStrict:row. The pi-ai payload fixup is simplified to the unconditional scrub of pi-ai's own per-tool strict default (pi-ai stampsstrict: falseon 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; bothGenerateOptionsandToolSchemakeep their rows inscripts/type-equiv.manifest.json, since each type survives minus a field.
This RFC 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 RFC records (this one and the content-block vocabulary RFC's producer-gated consequence); a tool-schema-scoped rg strict returns only this RFC, 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".