@tanstack/ai
Version:
Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.
110 lines (100 loc) • 4.48 kB
text/typescript
import type { CapabilityHandle } from './capabilities'
import type { AnyChatMiddleware, ChatMiddleware } from './types'
import type { DefinedChatMiddleware } from './define'
/** Union of capability NAME literals from a tuple of handles. */
export type NamesOf<T extends ReadonlyArray<CapabilityHandle>> =
T[number]['capabilityName']
/** Names provided across a middleware array (imprecise middleware → `string`). */
export type ProvidedNames<TList extends ReadonlyArray<AnyChatMiddleware>> =
NonNullable<TList[number]['provides']> extends infer P
? P extends ReadonlyArray<CapabilityHandle>
? NamesOf<P>
: never
: never
/** Names required across a middleware array. */
export type RequiredNames<TList extends ReadonlyArray<AnyChatMiddleware>> =
NonNullable<TList[number]['requires']> extends infer P
? P extends ReadonlyArray<CapabilityHandle>
? NamesOf<P>
: never
: never
/**
* Branded marker surfaced when required capability names are missing from the
* provided set, so the compiler error names the gap instead of emitting an
* opaque "not assignable".
*/
export type MissingCapabilities<TMissing extends string> = {
// The human-readable message lives in the property KEY, so TypeScript's
// "Property '<key>' is missing in type ... but required in type ..." error
// prints the explanation instead of an opaque `__missingCapabilities`. The
// key distributes over a union of missing names (one required key each).
[K in `✖ Missing capability "${TMissing}": no configured middleware provides it. Add a middleware whose \`provides\` includes it (and, with createChatMiddleware().use(), order the provider before this consumer).`]: never
}
/**
* Missing capability names. When required names are imprecise (`string`, i.e.
* plain `ChatMiddleware` not authored via `defineChatMiddleware`), we cannot
* prove a gap, so we allow it (→ `never`). Otherwise the precise literals not
* present in the provided set.
*/
type MissingNames<TList extends ReadonlyArray<AnyChatMiddleware>> =
string extends RequiredNames<TList>
? never
: Exclude<RequiredNames<TList>, ProvidedNames<TList>>
/**
* Resolves to `TList` when coverage holds, otherwise to a `MissingCapabilities`
* marker (not assignable to a middleware array) — producing a compile error at
* the `middleware` option that names the missing capability.
*/
export type CheckCoverage<TList extends ReadonlyArray<AnyChatMiddleware>> = [
MissingNames<TList>,
] extends [never]
? TList
: MissingCapabilities<MissingNames<TList>>
/**
* Order-aware middleware builder. Each `.use()` requires that the middleware's
* required capability names are already in the accumulated provided set, then
* adds its provided names. `.build()` returns the ordered array.
*
* `TProvided` is the running union of provided capability name literals.
*/
export interface ChatMiddlewareBuilder<
TList extends ReadonlyArray<AnyChatMiddleware>,
TProvided extends string,
> {
use: <
TRequires extends ReadonlyArray<CapabilityHandle>,
TProvides extends ReadonlyArray<CapabilityHandle>,
TContext = unknown,
>(
middleware: [NamesOf<TRequires>] extends [TProvided]
? DefinedChatMiddleware<TContext, TRequires, TProvides>
: DefinedChatMiddleware<TContext, TRequires, TProvides> &
MissingCapabilities<Exclude<NamesOf<TRequires>, TProvided>>,
) => ChatMiddlewareBuilder<
readonly [...TList, DefinedChatMiddleware<TContext, TRequires, TProvides>],
TProvided | NamesOf<TProvides>
>
build: () => [...TList]
}
/** Create an order-aware middleware builder. */
export function createChatMiddleware(): ChatMiddlewareBuilder<
readonly [],
never
> {
const list: Array<ChatMiddleware<unknown>> = []
const builder = {
use(middleware: ChatMiddleware<unknown>) {
list.push(middleware)
return builder
},
build() {
return list
},
}
// The only sanctioned assertion in this PR: the runtime `builder` is a single
// object reused across `.use()` calls, but the type accumulates `TProvided`
// and `TList` per call — TypeScript cannot derive that from runtime values, so
// a structural `as` is impossible and the double assertion is irreducible.
// eslint-disable-next-line no-restricted-syntax -- irreducible: type-level accumulation cannot be expressed from a single runtime object
return builder as unknown as ChatMiddlewareBuilder<readonly [], never>
}