docs: trim generated prose
This commit is contained in:
@@ -73,10 +73,8 @@ export class BlockAssembler {
|
||||
}
|
||||
case 'block-end': {
|
||||
const partial = this.ensure(chunk.index, chunk.block.type)
|
||||
// First close wins: a second block-end for an already-closed index is
|
||||
// a straggler (same rule as post-close deltas). Ignoring it keeps the
|
||||
// streamed prefix and the final blocks() in agreement — otherwise a
|
||||
// re-close could rewrite a block already flushed downstream.
|
||||
// First close wins: a second block-end for an already-closed index is a straggler (same
|
||||
// rule as post-close deltas).
|
||||
if (partial.block) return
|
||||
partial.block = chunk.block
|
||||
return chunk.block
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
/**
|
||||
* App-attribution vocabulary for provider requests.
|
||||
*
|
||||
* Every product LLM adapter must identify the application on every provider
|
||||
* HTTP request (see the adapter contract on {@link ../index.ts LlmAdapter}):
|
||||
* a static, non-secret product identity, sent as the standard `User-Agent`.
|
||||
* Adapters obtain the headers from {@link attributionHeaders} instead of
|
||||
* hand-copying constants, so the identity cannot drift between
|
||||
* implementations. The policy and its rationale are pinned in
|
||||
* docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-llm/attribution
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
/**
|
||||
* The call configuration of a conversation and its comparison/freeze
|
||||
* utilities. `LlmCallConfig` is the non-content third of the request header
|
||||
* (see `EpochHeader` in dsh-session): everything about a request besides its
|
||||
* message content that can undermine provider KV-cache reuse — `model`
|
||||
* selects the cache namespace outright, and the sampling scalars are treated
|
||||
* the same way out of caution. It is per-conversation state recorded in the
|
||||
* session log (the reconstructability RFC), never a silently-drifting
|
||||
* per-call knob: the `agent/request` waterfall proposes a replacement, and
|
||||
* the loop logs a real change as a `request/header-delta` event.
|
||||
*
|
||||
* The call configuration of a conversation and its comparison/freeze utilities.
|
||||
* @module dsh-llm/call-config
|
||||
*/
|
||||
|
||||
@@ -39,16 +30,9 @@ export function callConfigEquals(a: LlmCallConfig, b: LlmCallConfig): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep-freeze a value in place so any later mutation throws (ESM code runs in
|
||||
* strict mode), and return it. The loop freezes every request it builds
|
||||
* before dispatch — `llm/stream` listeners and adapters read the request,
|
||||
* never rewrite it, so the wire bytes cannot silently desync from what the
|
||||
* session log reconstructs. Guards against cycles with a WeakSet: loop-built
|
||||
* requests hold `structuredClone`d JSON-validated session data, but the
|
||||
* helper accepts arbitrarily constructed values. One exemption: an
|
||||
* `AbortSignal` is never entered or frozen — it is the request's live
|
||||
* cancellation channel, and freezing one breaks `AbortController.abort()`
|
||||
* outright (Node stores the aborted flag as an own property of the signal).
|
||||
* Deep-freeze a value in place so any later mutation throws (ESM code runs in strict mode),
|
||||
* and return it.
|
||||
*
|
||||
* @param value - the value to freeze in place.
|
||||
* @returns the same value, frozen.
|
||||
*/
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
/**
|
||||
* The harness error taxonomy: one base class so failures carry a stable,
|
||||
* machine-routable `code` and chain their `cause`, instead of flattening to a
|
||||
* bare message string. Per-package errors extend {@link HarnessError}; the
|
||||
* tool layer surfaces `{ name, code }` on results and the session `tool/result`
|
||||
* event so retry/sandbox plugins and replay can distinguish failure classes.
|
||||
*
|
||||
* Lives in dsh-llm (the leaf package every other imports) so a single base is
|
||||
* shared without a new dependency edge. See the error-taxonomy RFC.
|
||||
*
|
||||
* The harness error taxonomy: one base class so failures carry a stable, machine-routable
|
||||
* `code` and chain their `cause`, instead of flattening to a bare message string.
|
||||
* @module @deepseek-ai/dsh-llm/error
|
||||
*/
|
||||
|
||||
|
||||
@@ -55,22 +55,6 @@ export class LlmError extends HarnessError {
|
||||
|
||||
/**
|
||||
* Base class for LLM provider adapters.
|
||||
*
|
||||
* An adapter translates between the harness vocabulary (Message/ContentBlock/
|
||||
* StreamChunk) and one provider's wire format. Adapters register themselves
|
||||
* via `ctx.llm.registerAdapter(models, adapter)`.
|
||||
*
|
||||
* Real implementations: `@deepseek-ai/dsh-llm-deepseek` (hand-rolled
|
||||
* fetch/SSE) and `@deepseek-ai/dsh-llm-pi-ai` (pi-ai-backed) — two
|
||||
* deliberately different internals over the same contract; see the
|
||||
* adapter contract documented on `StreamChunk` in `./types.ts`.
|
||||
*
|
||||
* App attribution is part of the adapter contract: every HTTP request to a
|
||||
* provider carries the headers from `attributionHeaders()` (`./attribution.ts`)
|
||||
* — the standard `User-Agent` baseline everywhere. An adapter proves it with
|
||||
* a wire-level test (a mock server asserting the received header), or, for a
|
||||
* library-backed adapter, by asserting the library's header hook delivers the
|
||||
* same value to the wire.
|
||||
*/
|
||||
export abstract class LlmAdapter {
|
||||
/**
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
/**
|
||||
* Exhaustiveness helper for switches over core unions.
|
||||
*
|
||||
* # When to use which pattern
|
||||
*
|
||||
* **Closed unions** (every variant is known at compile time in the consuming
|
||||
* code — e.g. `StreamChunk` inside the assembler, `FiberState`-like enums):
|
||||
* end the switch with `default: assertNever(value)`. Adding a variant then
|
||||
* fails compilation at every switch that must handle it — the error appears
|
||||
* exactly where work is needed.
|
||||
*
|
||||
* **Merge-extensible unions** (plugins add variants via declaration merging —
|
||||
* `SessionEventMap`, `ContentBlockMap`, `MessageSourceMap`, …): do NOT use
|
||||
* assertNever. From the core's view the union is open; plugin-added variants
|
||||
* are valid values the core has never heard of. Handle the known cases and
|
||||
* fall through intentionally, with a comment saying the switch is
|
||||
* deliberately non-exhaustive (see `Session.deriveMessages`). The lint rule
|
||||
* `switch-exhaustiveness-check` enforces that the choice is explicit either
|
||||
* way.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-llm/never
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
/**
|
||||
* Provider-neutral message and streaming vocabulary.
|
||||
*
|
||||
* This is the canonical language spoken by the agent loop, session logs, and
|
||||
* every plugin. Adapters translate it to provider wire formats (DeepSeek V4
|
||||
* first); nothing outside an adapter should ever see a provider-specific
|
||||
* shape.
|
||||
*
|
||||
* Extensibility: the unions in this file are derived from interfaces
|
||||
* (`ContentBlockMap`, `MessageSourceMap`, `FinishReasonMap`) so that plugins
|
||||
* can extend them via declaration merging:
|
||||
*
|
||||
* ```ts
|
||||
* declare module '@deepseek-ai/dsh-llm' {
|
||||
* interface ContentBlockMap {
|
||||
* video: { type: 'video'; url: string }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||
@@ -126,25 +109,6 @@ export interface TokenUsage {
|
||||
|
||||
/**
|
||||
* Raw streaming protocol emitted by adapters.
|
||||
*
|
||||
* A streaming response interleaves several typed blocks (text, reasoning,
|
||||
* multiple tool calls); `index` ties each delta to its block, and `block-end`
|
||||
* carries the fully-assembled ContentBlock so consumers don't have to
|
||||
* re-assemble deltas themselves (use {@link BlockAssembler} when they do).
|
||||
*
|
||||
* Adapter contract — every adapter MUST obey these, and every consumer may
|
||||
* rely on them:
|
||||
* - Emit `usage` BEFORE `finish`, and nothing after `finish` (defer both to
|
||||
* the provider's end-of-stream marker so trailing usage-only chunks can't
|
||||
* violate this).
|
||||
* - Tool-call `arguments` stay RAW JSON strings end-to-end; partial fragments
|
||||
* stream via `argumentsDelta` (providers that hand back parsed objects
|
||||
* re-stringify at `block-end`).
|
||||
* - Failures may either THROW from `stream()` (transport/protocol errors) or
|
||||
* end the stream with `finish {kind:'error'|'aborted'}` (provider in-band
|
||||
* errors, for adapters that can't throw mid-stream); consumers must handle
|
||||
* both. The agent loop translates a finish-error/aborted into a turn error —
|
||||
* it never logs a normal completed assistant message for a failed step.
|
||||
*/
|
||||
export type StreamChunk =
|
||||
| { type: 'block-start'; index: number; blockType: ContentBlockType }
|
||||
@@ -193,17 +157,8 @@ export interface GenerateOptions {
|
||||
stop?: string[]
|
||||
signal?: AbortSignal
|
||||
/**
|
||||
* The id of the session this request belongs to — stamped by the agent loop
|
||||
* from `agent.session.id`. Adapters ignore it; it lets an `llm/stream` listener
|
||||
* route a call by WHICH session issued it (the replay adapter keys its per-call
|
||||
* cursor by session, so a parent and its in-process subagent — each with its
|
||||
* own session on one context — replay from their own recorded scripts).
|
||||
*
|
||||
* Typed as `Branded<'SessionId'>` rather than importing `SessionId` from
|
||||
* `dsh-session`: that package imports `Message` from here, so importing its
|
||||
* `SessionId` back would cycle. `SessionId` IS `Branded<'SessionId'>`, so a
|
||||
* real session id assigns with no cast. (A future ids package could own the
|
||||
* brand and dissolve this note.)
|
||||
* The id of the session this request belongs to — stamped by the agent loop from
|
||||
* `agent.session.id`.
|
||||
*/
|
||||
sessionId?: Branded<'SessionId'>
|
||||
}
|
||||
|
||||
@@ -63,10 +63,8 @@ describe('BlockAssembler', () => {
|
||||
|
||||
it('throws from assemble() when a partial has an unhandled blockType', () => {
|
||||
const assembler = new BlockAssembler()
|
||||
// A partial whose blockType is not text/reasoning/tool-call cannot be
|
||||
// assembled without its block-end. A plugin-added block type (here
|
||||
// 'video', via the merge-extensible ContentBlockMap) opened by a
|
||||
// block-start with no closing block-end exercises that throw.
|
||||
// A partial whose blockType is not text/reasoning/tool-call cannot be assembled without its
|
||||
// block-end.
|
||||
assembler.push({ type: 'block-start', index: 0, blockType: 'video' } as unknown as StreamChunk)
|
||||
expect(() => assembler.blocks()).toThrow('cannot assemble incomplete block of type "video"')
|
||||
})
|
||||
@@ -138,10 +136,7 @@ describe('assertNever', () => {
|
||||
describe('BlockAssembler regressions (property-test findings)', () => {
|
||||
it('first block-end wins: a duplicate block-end for a closed index is ignored', () => {
|
||||
// Found by fast-check (the property-testing RFC): two block-ends at the same index made the
|
||||
// streamed prefix (first block) disagree with final blocks() (second
|
||||
// block). The first close must win — same straggler rule as post-close
|
||||
// deltas — so the prefix returned incrementally by push() and the final
|
||||
// blocks() stay identical.
|
||||
// streamed prefix (first block) disagree with final blocks() (second block).
|
||||
const chunks: StreamChunk[] = [
|
||||
{ type: 'block-end', index: 0, block: { type: 'reasoning', text: 'first' } },
|
||||
{ type: 'block-end', index: 0, block: { type: 'text', text: 'second' } },
|
||||
|
||||
Reference in New Issue
Block a user