Document the codebase thoroughly and tighten type safety
Docs: per-folder README.md for packages/ (family overview + one per package: service, events, API, extension points, TODOs), examples/, and examples/echo-agent/; folder-level AGENTS.md (+ CLAUDE.md symlinks) for packages/ and vendor/; module-level doc comments in every packages/*/src file; richer JSDoc on all exported API (event side effects, disposal contracts, error behavior). Root AGENTS.md gains a "Type Safety and Documentation" policy section: the codebase aims to be very type-safe and well documented; type gymnastics are acceptable in core packages when they improve plugin-author DX; verbose docs are fine as long as they stay strictly in sync with the code. Type safety: removed the upstream-inherited "noImplicitAny": false from tsconfig.base.json — packages/* now compile under full strict mode; vendor/loader and vendor/include set it locally (vendor/cordis already did). Eliminated every `: any` / `as any` from packages and examples (catch clauses use unknown + a CodedError narrowing type; event data access uses discriminated-union narrowing). Typed tool schemas: new @deepseek-ai/dsh-tools schema DSL — SchemaSpec with per-property `required: true` booleans, type-level InferArgs<S>, a runtime SchemaSpec → JSON Schema converter, and defineTool() so first-party tools get typed execute(args) with zero casts (raw JSON Schema still accepted for MCP interop; chosen over schemastery because it targets JSON Schema generation directly). echo-tool and all test tools migrated; +7 tests.
This commit is contained in:
64
packages/llm/README.md
Normal file
64
packages/llm/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# 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): () => void`
|
||||
Register 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 `LlmAdapter` and call `ctx.llm.registerAdapter(models, adapter)`
|
||||
to add a new model provider.
|
||||
- Wrap `llm/stream` or `llm/generate` via `ctx.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 is `stream()`.
|
||||
- `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()`.
|
||||
- `LlmError` — typed error with a `code` string (`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, ...).
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Incremental chunk-to-message assembler. This is the single canonical assembly
|
||||
* algorithm used by both the agent loop and the LLM service convenience views.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-llm/assembler
|
||||
*/
|
||||
|
||||
import type { ContentBlock, FinishReason, GenerateResult, Message, StreamChunk, TokenUsage } from './types.ts'
|
||||
|
||||
interface PartialBlock {
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* LLM service: adapter registry with waterfall-interceptable streaming and
|
||||
* non-streaming call surfaces. Exports the `LlmService` default, the abstract
|
||||
* `LlmAdapter` for provider backends, and `BlockAssembler` for chunk assembly.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-llm
|
||||
*/
|
||||
|
||||
import { Context, Service } from 'cordis'
|
||||
import type { ContentBlock, GenerateOptions, GenerateResult, StreamChunk } from './types.ts'
|
||||
import { BlockAssembler } from './assembler.ts'
|
||||
@@ -20,6 +28,7 @@ declare module 'cordis' {
|
||||
}
|
||||
}
|
||||
|
||||
/** Typed error for LLM-related failures. The `code` string enables programmatic handling. */
|
||||
export class LlmError extends Error {
|
||||
constructor(message: string, public code: string) {
|
||||
super(message)
|
||||
@@ -53,7 +62,12 @@ export class LlmService extends Service {
|
||||
super(ctx, 'llm')
|
||||
}
|
||||
|
||||
/** Register an adapter for the given model names. Disposed with the fiber. */
|
||||
/**
|
||||
* Register an adapter for the given model names. Throws `LlmError` with code
|
||||
* `DUPLICATE_ADAPTER` if any model already has an adapter (all-or-nothing).
|
||||
* Emits `llm/adapter-change` on registration and disposal. Disposed with the
|
||||
* fiber.
|
||||
*/
|
||||
registerAdapter(models: string[], adapter: LlmAdapter): () => void {
|
||||
return this.ctx.effect(() => {
|
||||
for (const model of models) {
|
||||
@@ -81,7 +95,11 @@ export class LlmService extends Service {
|
||||
return adapter
|
||||
}
|
||||
|
||||
/** Stream one model call as raw chunks (token-level deltas). */
|
||||
/**
|
||||
* Stream one model call as raw chunks (token-level deltas). Throws
|
||||
* `LlmError` with code `NO_ADAPTER` if no adapter is registered for
|
||||
* `options.model`. Dispatches through the `llm/stream` waterfall.
|
||||
*/
|
||||
stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
return this.ctx.waterfall(this, 'llm/stream', options, () => {
|
||||
return this.adapter(options.model).stream(options)
|
||||
@@ -105,7 +123,11 @@ export class LlmService extends Service {
|
||||
yield * assembler.flushRemaining()
|
||||
}
|
||||
|
||||
/** One model call, fully assembled (drains the chunk stream). */
|
||||
/**
|
||||
* One model call, fully assembled (drains the chunk stream). Dispatches
|
||||
* through the `llm/generate` waterfall (and the inner stream through
|
||||
* `llm/stream`). Same completion guarantees as `streamBlocks()`.
|
||||
*/
|
||||
generate(options: GenerateOptions): Promise<GenerateResult> {
|
||||
return this.ctx.waterfall(this, 'llm/generate', options, async () => {
|
||||
const assembler = new BlockAssembler()
|
||||
|
||||
Reference in New Issue
Block a user