Flat config with two layers. Correctness (type-checked): the headline rules for this codebase are no-floating-promises / no-misused-promises (a lost promise in the agent loop is our primary bug class), switch-exhaustiveness-check (we switch over merge-extensible unions everywhere), no-unnecessary-condition, require-await, and no-explicit-any. Style (@stylistic): 2-space, no semicolons, single quotes, trailing commas, max-len 140 — the existing house style, now enforced instead of drifting between agents. vendor/ is excluded (vendored source keeps upstream style); tests relax the rules that fight test ergonomics (non-null assertions after expects, async mock signatures, non-Error throws). Code adjusted to pass: registry disposers wrap ctx.effect's promise-returning disposer behind a sync () => void (our public API), BlockAssembler gains an invariant-checking mustGet instead of non-null assertions, lastTurnNumber uses findLast, waterfall tails return Promise.resolve instead of async-without-await arrows, and the two deliberate suppressions (non-exhaustive derivation switch, unbound execute pass-through) carry justification comments. yarn lint / yarn lint:fix added.
dsh-llm
Provider-neutral LLM vocabulary and abstract service. This package defines the canonical language spoken by the agent loop, session logs, and every plugin.
Service: LlmService (ctx key: llm)
An adapter registry plus streaming / non-streaming call surfaces. Both call surfaces are interceptable via waterfall events.
Public API
ctx.llm.registerAdapter(models: string[], adapter: LlmAdapter): () => voidRegister an adapter for the given model names. Disposed with the calling fiber.ctx.llm.models(): string[]— model names with a registered adapter.ctx.llm.stream(options: GenerateOptions): AsyncIterable<StreamChunk>Stream one model call as raw chunks (token-level deltas).ctx.llm.streamBlocks(options: GenerateOptions): AsyncIterable<ContentBlock>Stream as completed content blocks (convenience view).ctx.llm.generate(options: GenerateOptions): Promise<GenerateResult>One model call, fully assembled.
Events
| Event | Mode | Purpose |
|---|---|---|
llm/stream |
waterfall | Intercept/wrap every streaming model call (retry, caching, routing) |
llm/generate |
waterfall | Intercept/wrap every non-streaming model call |
llm/adapter-change |
emit | An adapter was registered or unregistered |
Extension points
- Subclass
LlmAdapterand callctx.llm.registerAdapter(models, adapter)to add a new model provider. - Wrap
llm/streamorllm/generateviactx.on()waterfall listeners for caching, retry, logging, rate-limiting, etc.
Content-block vocabulary (types.ts)
Messages are arrays of typed content blocks: text, reasoning, tool-call,
tool-result, image. The union is derived from the merge-extensible
ContentBlockMap, so plugins can add block types via declaration merging.
Streaming is a raw chunk protocol (block-start, text-delta,
reasoning-delta, tool-call-delta, block-end, usage, finish).
BlockAssembler is the single shared implementation that assembles chunks into
blocks/messages.
Classes
LlmAdapter— abstract base class for provider adapters. The only required method isstream().BlockAssembler— incrementally assembles raw chunks into complete content blocks and an assistant message. Used by the agent loop (raw chunks for replay- assembled for history) and by
streamBlocks()/generate().
- assembled for history) and by
LlmError— typed error with acodestring (NO_ADAPTER,DUPLICATE_ADAPTER).
What is NOT here (TODO)
- DeepSeek V4 adapter — the first real adapter lands in a later phase.
- Streaming protocol review — the chunk protocol has
TODO(review)markers and needs careful review before the first real adapter (DeepSeek V4 wire format, partial JSON arguments, interleaved reasoning signatures, ...).