refactor: prune unused llm contract fields

This commit is contained in:
Tianyi Cui
2026-07-14 03:58:34 +08:00
parent a0359bc4a9
commit 5c82310f47
13 changed files with 37 additions and 64 deletions

View File

@@ -42,7 +42,7 @@ Every product adapter must identify the application on every provider HTTP reque
- `LlmAdapter` — abstract base class for provider adapters. The only required method is `stream()`.
- `BlockAssembler` — incrementally assembles raw chunks into complete content blocks and an assistant message. The agent loop feeds it raw chunks (logging them for replay) while reading the assembled blocks/message for history.
- `HarnessError` — base class for the harness error taxonomy: a stable `code` string (distinct from the human `message`) plus `cause` chaining. Lives here, in the leaf package every other imports, so a single base is shared without a new dependency edge. Per-package errors (`LlmError`, `ToolArgsError`, `InvariantError`, …) extend it. `isHarnessError(value)` narrows at seams.
- `LlmError` — extends `HarnessError`; `code` string (`NO_ADAPTER`, `DUPLICATE_ADAPTER`, and adapter codes like `AUTH`/`RATE_LIMIT`) plus an optional numeric `status` when the failure came from a non-2xx provider response.
- `LlmError` — extends `HarnessError`; its stable `code` string (`NO_ADAPTER`, `DUPLICATE_ADAPTER`, and adapter codes like `AUTH`/`RATE_LIMIT`) is the programmatic failure contract.
### Real adapters

View File

@@ -38,12 +38,10 @@ export class BlockAssembler {
private _finish: FinishReason | undefined
/**
* Feed one chunk. Returns the completed block when the chunk closes one
* (an explicit `block-end`), otherwise undefined.
* Feed one chunk into the assembly state.
* @param chunk - the next raw chunk, in stream order.
* @returns the authoritative block from the first `block-end` at its index; undefined for every other chunk.
*/
push(chunk: StreamChunk): ContentBlock | undefined {
push(chunk: StreamChunk): void {
switch (chunk.type) {
case 'block-start': {
if (!this.partials.has(chunk.index)) {
@@ -79,7 +77,7 @@ export class BlockAssembler {
// re-close could rewrite a block already flushed downstream.
if (partial.block) return
partial.block = chunk.block
return chunk.block
return
}
case 'usage': {
this._usage = chunk.usage

View File

@@ -42,12 +42,10 @@ declare module 'cordis' {
/**
* Typed error for LLM-related failures. Extends {@link HarnessError}, so the
* `code` string (e.g. `AUTH`, `RATE_LIMIT`, `NO_ADAPTER`) is shared taxonomy;
* `status` carries the HTTP status when the error originated from a non-2xx
* provider response (absent for protocol/usage errors that have no HTTP status).
* `code` string (e.g. `AUTH`, `RATE_LIMIT`, `NO_ADAPTER`) is shared taxonomy.
*/
export class LlmError extends HarnessError {
constructor(message: string, code: string, public status?: number, options?: ErrorOptions) {
constructor(message: string, code: string, options?: ErrorOptions) {
super(message, code, options)
this.name = 'LlmError'
}

View File

@@ -29,12 +29,12 @@ describe('BlockAssembler', () => {
expect(assembler.message().role).toBe('assistant')
})
it('returns the completed block from push() on block-end', () => {
it('records the completed block from block-end', () => {
const assembler = new BlockAssembler()
expect(assembler.push({ type: 'block-start', index: 0, blockType: 'text' })).toBeUndefined()
expect(assembler.push({ type: 'text-delta', index: 0, text: 'hi' })).toBeUndefined()
const block = assembler.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'hi' } })
expect(block).toEqual({ type: 'text', text: 'hi' })
assembler.push({ type: 'block-start', index: 0, blockType: 'text' })
assembler.push({ type: 'text-delta', index: 0, text: 'hi' })
assembler.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'hi' } })
expect(assembler.blocks()).toEqual([{ type: 'text', text: 'hi' }])
})
it('tolerates deltas without explicit block-start/end', () => {
@@ -57,8 +57,8 @@ describe('BlockAssembler', () => {
// push a delta first to guarantee the partial exists
assembler.push({ type: 'text-delta', index: 0, text: 'hi' })
// block-end's ensure() must find the existing partial (the second branch path)
const block = assembler.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'hi' } })
expect(block).toEqual({ type: 'text', text: 'hi' })
assembler.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'hi' } })
expect(assembler.blocks()).toEqual([{ type: 'text', text: 'hi' }])
})
it('throws from assemble() when a partial has an unhandled blockType', () => {
@@ -130,7 +130,7 @@ describe('assertNever', () => {
it('BlockAssembler.push rejects chunks outside the closed StreamChunk union', () => {
const assembler = new BlockAssembler()
expect(() => assembler.push({ type: 'rogue-chunk' } as unknown as StreamChunk))
expect(() => { assembler.push({ type: 'rogue-chunk' } as unknown as StreamChunk) })
.toThrow('unreachable variant in BlockAssembler.push')
})
})
@@ -140,32 +140,13 @@ describe('BlockAssembler regressions (property-test findings)', () => {
// 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.
// deltas — so later chunks cannot rewrite the completed block.
const chunks: StreamChunk[] = [
{ type: 'block-end', index: 0, block: { type: 'reasoning', text: 'first' } },
{ type: 'block-end', index: 0, block: { type: 'text', text: 'second' } },
]
const streaming = new BlockAssembler()
const closed = []
for (const chunk of chunks) {
const block = streaming.push(chunk)
if (block) closed.push(block)
}
const oneShot = new BlockAssembler()
for (const chunk of chunks) oneShot.push(chunk)
expect(closed).toEqual([{ type: 'reasoning', text: 'first' }])
expect(oneShot.blocks()).toEqual([{ type: 'reasoning', text: 'first' }])
expect(closed).toEqual(oneShot.blocks())
})
it('push returns undefined for a duplicate block-end (it closed nothing)', () => {
const a = new BlockAssembler()
expect(a.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'x' } }))
.toEqual({ type: 'text', text: 'x' })
expect(a.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'y' } }))
.toBeUndefined()
const assembler = new BlockAssembler()
for (const chunk of chunks) assembler.push(chunk)
expect(assembler.blocks()).toEqual([{ type: 'reasoning', text: 'first' }])
})
})

View File

@@ -79,11 +79,12 @@ describe('LlmService', () => {
it('LlmError extends the shared HarnessError base', async () => {
const { HarnessError, isHarnessError } = await import('@deepseek-ai/dsh-llm')
const err = new LlmError('boom', 'AUTH', 401)
const cause = new Error('root cause')
const err = new LlmError('boom', 'AUTH', { cause })
expect(err).toBeInstanceOf(HarnessError)
expect(isHarnessError(err)).toBe(true)
expect(err.code).toBe('AUTH')
expect(err.status).toBe(401)
expect(err.cause).toBe(cause)
})
it('HarnessError carries a code, names itself by subclass, and chains cause', async () => {