Architecture
One declaration,
three consumers.
A signature drives the prompt, the JSON Schema sent to the provider, and the zod schema every reply is checked against. Everything below follows from that single source.
01
One request, end to end
A signature class records its fields on the constructor at decoration
time, so getInputFields() and getOutputFields()
work without ever instantiating it. Predict.forward() reads
those fields three separate times: once to write the prompt, once to build
a JSON Schema for providers that can constrain decoding, and once to build
the zod schema that every reply must pass.
The fork in the middle is the only place the two paths differ. They rejoin at the same gate, so a model that answers in prose where you declared a number fails identically whichever route it took.
safeParse, so nothing
reaches your code untyped. All three shipped providers report
supportsStructuredOutput: true, so the right-hand branch is what
a custom ILanguageModel gets — and what RespAct
always uses.
02
Packages and the provider seam
@ts-dspy/core depends on zod and nothing else —
no vendor SDK. Each provider package pulls in its own official SDK and
extends BaseLM, which already owns usage accounting,
generate() delegation, and a prompt-based
generateStructured() fallback. A provider implements two
methods: chat() and getCapabilities().
Modules never name a vendor. They hold an ILanguageModel,
handed in at construction or read from the configure() singleton.
ILanguageModel. Core
calls down to the interface; providers implement up to it. That is why
installing @ts-dspy/core alone pulls in no vendor SDK, and why a
fourth provider is a new package rather than a change to core.
03
What each module adds
All three extend Module, which supplies the language model and
the forward() contract. call() and
__call__() are aliases for it.
| Module | LM calls | Adds to the output | What it is for |
|---|---|---|---|
| Predict | 1 | — | The baseline. Prompt, one completion, validate. |
| ChainOfThought | 2 | reasoning: string | Extends Predict. Step one reasons in free text via plain generate(); step two re-asks with that reasoning in context and validates normally. |
| RespAct | 1…maxSteps | steps: number | Extends Module directly. A reason–act–observe loop over tools you supply, with an onEvent hook for tracing. |
04
The RespAct loop
RespAct is the only module whose control flow is a cycle rather
than a line. The transcript grows in place: each thought, each observation,
and each correction is appended to the same conversation string and re-sent.
Two edges matter more than the happy path. A tool call identical to one already made gets an observation telling the model to move on, instead of being executed again. And a malformed final answer is not fatal — the loop names the fields that failed and gives the model another step, provided one is left.
05
Where things live
| Path | Holds |
|---|---|
| core/src/core/signature.ts | Decorators and the static field maps. |
| core/src/core/module.ts | The forward() contract and LM resolution. |
| core/src/core/base-lm.ts | Usage accounting, the structured-output fallback, and fence-tolerant JSON extraction. |
| core/src/core/config.ts | The configure() singleton: default LM, cache flag, tracing flag. |
| core/src/utils/schema.ts | Type string → zod schema, and → JSON Schema. Lenient coercion lives here. |
| core/src/utils/parsing.ts | Prompt construction and the labelled-text extractor. |
| core/src/core/errors.ts | TsDspyError → ValidationError, LMError. |
| {openai,gemini,anthropic}/src | One BaseLM subclass each, plus its native structured-output override. |
Coercion is deliberately lenient and validation is deliberately loud.
"42" satisfies a number field, and so does
"1,200" and "87%" — commas and a trailing
percent are stripped before the check. But "about forty" throws
a ValidationError naming the field rather than handing back a
string where the type says number. Versions before 0.5.0 returned the raw
string on coercion failure, which is exactly the lie the runtime check
exists to prevent.