docs: rebalance prose cleanup and add trimming skill
This commit is contained in:
@@ -73,9 +73,8 @@ export class DeepSeekAdapter extends LlmAdapter {
|
||||
const parsed = await response.json() as WireError
|
||||
if (parsed.error?.message) message = parsed.error.message
|
||||
} catch {
|
||||
// Paranoid by design: `code` and the HTTP status are ALREADY captured above (and passed
|
||||
// to LlmError below), so the only thing this `try` can add is a richer
|
||||
// provider-supplied message.
|
||||
// Only swallow error-body parsing: status and code are already captured,
|
||||
// so malformed gateway JSON must not mask the actionable HTTP failure.
|
||||
}
|
||||
throw new LlmError(message, code, response.status)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* DeepSeek LLM adapter plugin: registers a {@link DeepSeekAdapter} for the configured model
|
||||
* names on `ctx.llm`.
|
||||
* Register a {@link DeepSeekAdapter} for configured model names on `ctx.llm`. Configuration uses
|
||||
* Cordis schemastery; pass secrets from environment variables through `cordis.yml` with `!!js`,
|
||||
* as shown in the package README, rather than reading ad hoc files.
|
||||
* @module @deepseek-ai/dsh-llm-deepseek
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Serialize harness vocabulary (`GenerateOptions`, `Message[]`) into the DeepSeek
|
||||
* chat-completions request body.
|
||||
* Serialize harness messages into DeepSeek chat completions. User text is joined; assistant text
|
||||
* becomes `content`, tool calls become `tool_calls`, and tool results become separate tool messages.
|
||||
* Assistant reasoning is replayed as `reasoning_content` only on tool-call turns, as required by
|
||||
* thinking-mode passback. Unknown declaration-merged block types are skipped rather than rejected.
|
||||
* @module dsh-llm-deepseek/serialize
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
/**
|
||||
* Decode an SSE byte stream into event `data` payloads. Network reads may split UTF-8 or lines;
|
||||
* CRLF, comments, non-data fields, and multi-data events are handled per SSE rules. The literal
|
||||
* `[DONE]` is yielded so the caller owns final flushing, and EOF before it raises {@link LlmError}.
|
||||
*
|
||||
* Minimal SSE (text/event-stream) parser for the chat-completions stream.
|
||||
* @module dsh-llm-deepseek/sse
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/**
|
||||
* Translate DeepSeek SSE payloads with one stateful harness block per content, reasoning, or tool
|
||||
* call index. An empty initial reasoning delta does not open a block. Finish reason and the latest
|
||||
* usage are deferred until `[DONE]`, covering both finish-attached and trailing usage-only shapes
|
||||
* while ensuring no chunk follows `finish`.
|
||||
*
|
||||
* Translate DeepSeek wire chunks into the harness `StreamChunk` protocol.
|
||||
* @module dsh-llm-deepseek/translate
|
||||
*/
|
||||
|
||||
@@ -187,7 +187,7 @@ describe('serializeRequest', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('review fixes: assistant content shapes', () => {
|
||||
describe('assistant empty and tool-call content shapes', () => {
|
||||
it('serializes a content-less, tool-call-less assistant message as null content', () => {
|
||||
// Aborted/empty assistant turns: no text, no calls → null (the wire
|
||||
// accepts it; "" is reserved for tool-call turns per the samples).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* `PiAiAdapter`: the `@earendil-works/pi-ai`-backed implementation of the harness LLM seam,
|
||||
* pointed at a DeepSeek (OpenAI-compatible) endpoint.
|
||||
* Pi-ai-backed DeepSeek adapter and design twin of the hand-rolled adapter.
|
||||
* Both implementations must fit the same provider-neutral stream vocabulary.
|
||||
* @module dsh-llm-pi-ai/adapter
|
||||
*/
|
||||
|
||||
@@ -37,8 +37,8 @@ export function buildModel(modelId: string, options: PiAiAdapterOptions): Model<
|
||||
api: 'openai-completions',
|
||||
provider: 'deepseek',
|
||||
baseUrl: options.baseURL,
|
||||
// Always true: pi-ai only emits the DeepSeek `thinking` field for reasoning-capable models,
|
||||
// deriving enabled/disabled from whether a reasoningEffort option is passed.
|
||||
// Keep reasoning support enabled so `off` can send DeepSeek's explicit
|
||||
// disabled marker rather than falling back to the provider's enabled default.
|
||||
reasoning: true,
|
||||
// DeepSeek's official effort levels: high|max (xhigh maps to max).
|
||||
thinkingLevelMap: { minimal: null, low: null, medium: null, high: 'high', xhigh: 'max' },
|
||||
@@ -143,8 +143,8 @@ export class PiAiAdapter extends LlmAdapter {
|
||||
// `reasoning_effort` so the provider chooses its default effort.
|
||||
const reasoning = this.options.reasoning ?? 'high'
|
||||
|
||||
// pi-ai's event stream has no iterator-return cancellation hook: if our consumer stops
|
||||
// early (break / loop abort), the underlying HTTP stream would keep draining.
|
||||
// Pi-ai has no iterator-return cancellation hook. Chain an internal signal
|
||||
// and abort it when this generator exits so early consumers stop the HTTP stream.
|
||||
const controller = new AbortController()
|
||||
const onCallerAbort = (): void => { controller.abort(options.signal?.reason) }
|
||||
if (options.signal?.aborted) controller.abort(options.signal.reason)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/**
|
||||
* Bidirectional mapping between the harness vocabulary and pi-ai's:
|
||||
* `GenerateOptions`/`Message[]` → pi-ai `Context`, and pi-ai `AssistantMessageEvent`s →
|
||||
* harness `StreamChunk`s.
|
||||
* Convert harness requests to pi-ai context and pi-ai assistant events to harness stream chunks.
|
||||
* pi-ai parses tool arguments while the harness preserves raw JSON, so conversion parses inbound
|
||||
* arguments and re-stringifies outbound values while the adapter restores provider payloads.
|
||||
* In-stream pi-ai errors become harness error/aborted finishes, and its reasoning tokens remain
|
||||
* folded into output usage because it reports no separate count.
|
||||
* @module dsh-llm-pi-ai/convert
|
||||
*/
|
||||
|
||||
@@ -65,7 +68,8 @@ export function toPiContext(options: GenerateOptions): PiContext {
|
||||
content.push({ type: 'text', text: block.text })
|
||||
break
|
||||
case 'reasoning':
|
||||
// thinkingSignature names the wire field pi-ai replays the CoT under.
|
||||
// Without this wire-field name, pi-ai replays an empty `reasoning_content`, violating
|
||||
// DeepSeek's thinking-mode passback rule on tool-call turns.
|
||||
content.push({ type: 'thinking', thinking: block.text, thinkingSignature: 'reasoning_content' })
|
||||
break
|
||||
case 'tool-call':
|
||||
|
||||
@@ -312,7 +312,7 @@ describe('buildModel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('review fixes', () => {
|
||||
describe('provider reasoning, passback, and early-stream cancellation', () => {
|
||||
it('defaults omitted reasoning config to thinking ENABLED (provider default)', async () => {
|
||||
const server = await mockServer([{ events: textEvents }])
|
||||
const ctx = await harness(server.url) // no reasoning key at all
|
||||
@@ -375,7 +375,7 @@ describe('review fixes', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('review fixes: abort wiring', () => {
|
||||
describe('caller cancellation', () => {
|
||||
it('honors a pre-aborted caller signal', async () => {
|
||||
const ctx = await harness('http://127.0.0.1:1')
|
||||
const controller = new AbortController()
|
||||
|
||||
@@ -46,4 +46,4 @@ Every product adapter sends application identity on provider HTTP requests. `att
|
||||
|
||||
### Real adapters
|
||||
|
||||
Two adapters implement `LlmAdapter` against this vocabulary, deliberately built on different internals to keep the contract honest (see [the twin LLM adapters](../../../docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md)): [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) (hand-rolled fetch/SSE) and [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) (via `@earendil-works/pi-ai`). The pair pinned down the `StreamChunk` conventions now documented in `types.ts` (usage before finish, raw-string tool arguments, the two sanctioned error paths).
|
||||
Two adapters implement `LlmAdapter` on different internals: [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) uses hand-rolled fetch/SSE, while [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) uses `@earendil-works/pi-ai`. Both follow the `StreamChunk` conventions in `types.ts`: usage precedes finish, tool arguments remain raw strings, and errors take one of two sanctioned paths. See [the twin LLM adapters](../../../docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the design rationale.
|
||||
|
||||
@@ -73,8 +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).
|
||||
// First close wins; ignoring re-close stragglers keeps streamed output
|
||||
// and the final assembled block in agreement.
|
||||
if (partial.block) return
|
||||
partial.block = chunk.block
|
||||
return chunk.block
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
/**
|
||||
* Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
|
||||
* adapters from drifting. See
|
||||
* `docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
|
||||
*
|
||||
* App-attribution vocabulary for provider requests.
|
||||
* @module @deepseek-ai/dsh-llm/attribution
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/**
|
||||
* The call configuration of a conversation and its comparison/freeze utilities.
|
||||
* Conversation call configuration and freeze utilities. Model and sampling
|
||||
* values are request-header state that can affect cache reuse; request
|
||||
* waterfalls replace them and the loop logs changes instead of allowing
|
||||
* silent per-call drift.
|
||||
* @module dsh-llm/call-config
|
||||
*/
|
||||
|
||||
@@ -30,9 +33,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.
|
||||
*
|
||||
* Deep-freeze a value in place, guarding cycles, so later mutation throws.
|
||||
* {@link AbortSignal} objects are deliberately skipped because they are the
|
||||
* request's live cancellation channel and freezing them breaks abort.
|
||||
* @param value - the value to freeze in place.
|
||||
* @returns the same value, frozen.
|
||||
*/
|
||||
|
||||
@@ -1,6 +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.
|
||||
* Harness error base with a stable machine-routable code and chained cause.
|
||||
* Package errors extend it so tool results and replay can retain failure class.
|
||||
* @module @deepseek-ai/dsh-llm/error
|
||||
*/
|
||||
|
||||
|
||||
@@ -54,7 +54,10 @@ export class LlmError extends HarnessError {
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for LLM provider adapters.
|
||||
* Provider-wire adapter for the harness message and stream vocabulary. Register implementations
|
||||
* with `ctx.llm.registerAdapter(models, adapter)`. Every provider HTTP request must include
|
||||
* `attributionHeaders()`; prove that at the wire or library header-hook boundary. The hand-rolled
|
||||
* DeepSeek and pi-ai adapters intentionally exercise this contract through different internals.
|
||||
*/
|
||||
export abstract class LlmAdapter {
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
/**
|
||||
* Exhaustiveness helper for switches over core unions.
|
||||
* Exhaustiveness helper for closed core unions. Use {@link assertNever} at the default branch so a
|
||||
* new variant fails compilation at every required handler. Do not use it for declaration-merged
|
||||
* unions such as session events or content blocks: handle known variants and explicitly fall
|
||||
* through because plugins may add valid unknown cases.
|
||||
* @module @deepseek-ai/dsh-llm/never
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mark an unreachable closed-union branch and diagnose values that escaped
|
||||
* static exhaustiveness.
|
||||
* Mark an unreachable closed-union branch. A newly unhandled typed variant fails at the call site;
|
||||
* a value that escaped its type throws with diagnostics at runtime.
|
||||
* @param value - the impossible value; typed `never` so an unhandled variant fails compilation at the call site.
|
||||
* @param context - optional label (e.g. the switch site) prefixed into the throw message.
|
||||
* @returns never — it always throws, with the offending value JSON-rendered in the message.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* Provider-neutral message and streaming vocabulary.
|
||||
* Canonical provider-neutral message and streaming vocabulary for the loop,
|
||||
* session log, and plugins. Adapters alone translate provider wire shapes;
|
||||
* mapped interfaces make the content, source, and finish unions extensible.
|
||||
*/
|
||||
|
||||
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||
@@ -102,6 +104,10 @@ export interface TokenUsage {
|
||||
|
||||
/**
|
||||
* Raw streaming protocol emitted by adapters.
|
||||
* Block indexes correlate interleaved deltas, and `block-end` carries the
|
||||
* assembled block. Adapters emit usage before the terminal finish and nothing
|
||||
* afterward; tool arguments remain raw JSON strings. Failures either throw or
|
||||
* end with `error`/`aborted`, and consumers must handle both paths.
|
||||
*/
|
||||
export type StreamChunk =
|
||||
| { type: 'block-start'; index: number; blockType: ContentBlockType }
|
||||
@@ -150,8 +156,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`.
|
||||
* Session identity stamped by the loop for listener routing. Adapters ignore
|
||||
* it; replay uses it to keep concurrent parent and child cursors independent.
|
||||
*/
|
||||
sessionId?: Branded<'SessionId'>
|
||||
}
|
||||
|
||||
@@ -63,8 +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.
|
||||
// Unknown declaration-merged block types cannot be assembled from partial deltas. Opening a
|
||||
// plugin-added `video` block without its required `block-end` exercises that failure.
|
||||
assembler.push({ type: 'block-start', index: 0, blockType: 'video' } as unknown as StreamChunk)
|
||||
expect(() => assembler.blocks()).toThrow('cannot assemble incomplete block of type "video"')
|
||||
})
|
||||
@@ -133,10 +133,9 @@ describe('assertNever', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('BlockAssembler regressions (property-test findings)', () => {
|
||||
describe('BlockAssembler duplicate-close contract', () => {
|
||||
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 wins so streamed and final output cannot disagree.
|
||||
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