@jaex/dstsx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,2089 @@
1
+ # DSTsx
2
+
3
+ > A TypeScript-first port of [DSPy](https://github.com/stanfordnlp/dspy) — Declarative Self-improving Language Programs.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/dstsx.svg)](https://www.npmjs.com/package/dstsx)
6
+ [![license](https://img.shields.io/npm/l/dstsx.svg)](LICENSE)
7
+ [![tests](https://img.shields.io/badge/tests-218%20passing-brightgreen.svg)](#)
8
+
9
+ DSTsx lets you build **typed, composable LM pipelines** in TypeScript and then **optimize** their prompts and few-shot examples automatically—no manual prompt engineering required.
10
+
11
+ > **LLM / AI assistant?** See [`llms.txt`](./llms.txt) for a machine-readable index, or browse the focused [`docs/`](./docs/) directory for topic-specific reference files.
12
+
13
+ ---
14
+
15
+ ## Table of Contents
16
+
17
+ 1. [Installation](#installation)
18
+ 2. [Quick Start](#quick-start)
19
+ 3. [Core Concepts](#core-concepts)
20
+ 4. [API Reference](#api-reference)
21
+ - [Signatures](#signatures)
22
+ - [Primitives — Example, Prediction, Trace](#primitives--example-prediction-trace)
23
+ - [Language Model Adapters](#language-model-adapters)
24
+ - [Settings & Context](#settings--context)
25
+ - [Modules](#modules)
26
+ - [Retrievers](#retrievers)
27
+ - [Optimizers](#optimizers)
28
+ - [Evaluation](#evaluation)
29
+ - [Assertions & Suggestions](#assertions--suggestions)
30
+ 5. [V2 APIs](#v2-apis)
31
+ - [TypedPredictor & TypedChainOfThought](#typedpredictor--typedchainofthought)
32
+ - [Parallel Module](#parallel-module)
33
+ - [Refine Module](#refine-module)
34
+ - [majority() Helper](#majority-helper)
35
+ - [BootstrapFewShotWithOptuna](#bootstrapfewshotwithoptuna)
36
+ - [Disk-Persistent LM Cache](#disk-persistent-lm-cache)
37
+ - [MCP Integration](#mcp-integration)
38
+ - [LM Streaming](#lm-streaming)
39
+ - [NativeReAct](#nativereact)
40
+ - [Image — Multi-modal Support](#image--multi-modal-support)
41
+ - [BootstrapFinetune](#bootstrapfinetune)
42
+ - [GRPO Optimizer](#grpo-optimizer)
43
+ - [SIMBA Optimizer](#simba-optimizer)
44
+ - [AvatarOptimizer](#avataroptimizer)
45
+ - [Experiment Tracking](#experiment-tracking)
46
+ - [Worker-Thread ProgramOfThought](#worker-thread-programofthought)
47
+ 6. [End-to-End Examples](#end-to-end-examples)
48
+ 7. [V2 Roadmap](#v2-roadmap)
49
+
50
+ ---
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ npm install dstsx
56
+ ```
57
+
58
+ Install provider SDK peer dependencies only for the adapters you use:
59
+
60
+ ```bash
61
+ # OpenAI
62
+ npm install openai
63
+
64
+ # Anthropic
65
+ npm install @anthropic-ai/sdk
66
+
67
+ # Cohere
68
+ npm install cohere-ai
69
+
70
+ # Google Generative AI
71
+ npm install @google/generative-ai
72
+
73
+ # Vector store retrievers (pick what you need)
74
+ npm install @pinecone-database/pinecone
75
+ npm install chromadb
76
+ npm install @qdrant/js-client-rest
77
+ npm install weaviate-client
78
+
79
+ # MCP (Model Context Protocol) integration — optional
80
+ npm install @modelcontextprotocol/sdk
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Quick Start
86
+
87
+ ```ts
88
+ import { settings, OpenAI, Predict } from "dstsx";
89
+
90
+ // 1. Configure the global LM
91
+ settings.configure({ lm: new OpenAI({ model: "gpt-4o" }) });
92
+
93
+ // 2. Define a module
94
+ const qa = new Predict("question -> answer");
95
+
96
+ // 3. Run it
97
+ const result = await qa.forward({ question: "What is the capital of France?" });
98
+ console.log(result.get("answer")); // "Paris"
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Core Concepts
104
+
105
+ | Concept | Description |
106
+ |---|---|
107
+ | **Signature** | Typed interface (inputs → outputs) for one LM call |
108
+ | **Example** | Immutable key-value training record |
109
+ | **Prediction** | Module output extending Example with `completions` |
110
+ | **Trace** | Record of a single LM call (inputs, outputs, usage, latency) |
111
+ | **LM** | Abstract language model adapter |
112
+ | **Module** | Composable unit containing one or more LM calls |
113
+ | **Retriever** | Abstract vector-store backend |
114
+ | **Optimizer** | Automatically tunes demos/instructions of a Module |
115
+ | **Metric** | Scoring function for Evaluate and Optimizers |
116
+
117
+ ---
118
+
119
+ ## API Reference
120
+
121
+ ### Signatures
122
+
123
+ Signatures declare the **typed input/output interface** for a single LM call.
124
+
125
+ #### `Signature.from(shorthand, instructions?)`
126
+
127
+ Parse a shorthand string. Use `->` to separate inputs from outputs; suffix `?` for optional fields.
128
+
129
+ ```ts
130
+ import { Signature } from "dstsx";
131
+
132
+ // Simple shorthand
133
+ const sig = Signature.from("question -> answer");
134
+
135
+ // Multiple fields, optional field, instructions
136
+ const sig2 = Signature.from(
137
+ "context, question -> answer, confidence?",
138
+ "Answer based only on the provided context."
139
+ );
140
+ ```
141
+
142
+ #### `new Signature(meta)`
143
+
144
+ Construct a signature explicitly with full field metadata.
145
+
146
+ ```ts
147
+ import { Signature, InputField, OutputField } from "dstsx";
148
+
149
+ const sig = new Signature({
150
+ inputs: new Map([
151
+ ["context", InputField({ description: "Background passages" })],
152
+ ["question", InputField({ description: "The question to answer" })],
153
+ ]),
154
+ outputs: new Map([
155
+ ["answer", OutputField({ description: "Concise factual answer", type: "string" })],
156
+ ]),
157
+ instructions: "Answer using only the context provided.",
158
+ });
159
+ ```
160
+
161
+ #### `InputField(meta?)` / `OutputField(meta?)`
162
+
163
+ Builder helpers that return a `FieldMeta` descriptor.
164
+
165
+ ```ts
166
+ import { InputField, OutputField } from "dstsx";
167
+
168
+ const field = InputField({
169
+ description: "The user's question",
170
+ prefix: "Q:", // optional prompt prefix
171
+ format: "markdown", // optional format hint
172
+ optional: true, // field may be absent
173
+ type: "string", // "string" | "number" | "boolean" | "string[]" | "object"
174
+ });
175
+ ```
176
+
177
+ #### `FieldMeta` interface
178
+
179
+ ```ts
180
+ interface FieldMeta {
181
+ description?: string;
182
+ prefix?: string;
183
+ format?: string;
184
+ optional?: boolean;
185
+ type?: "string" | "number" | "boolean" | "string[]" | "object";
186
+ }
187
+ ```
188
+
189
+ #### Signature mutation helpers (return new Signature, never mutate)
190
+
191
+ ```ts
192
+ const base = Signature.from("question -> answer");
193
+
194
+ // Add or override fields / instructions
195
+ const extended = base.with({ instructions: "Be concise." });
196
+
197
+ // Add a single input field
198
+ const withCtx = base.withInput("context", { description: "Background text" });
199
+
200
+ // Add a single output field
201
+ const withConf = base.withOutput("confidence", { type: "number" });
202
+ ```
203
+
204
+ #### Serialization
205
+
206
+ ```ts
207
+ const json = sig.toJSON(); // → plain object
208
+ const sig2 = Signature.fromJSON(json); // → Signature
209
+ ```
210
+
211
+ ---
212
+
213
+ ### Primitives — Example, Prediction, Trace
214
+
215
+ #### `Example`
216
+
217
+ Immutable record of named values used as training data or module inputs.
218
+
219
+ ```ts
220
+ import { Example } from "dstsx";
221
+
222
+ const ex = new Example({ question: "What is 2+2?", answer: "4" });
223
+
224
+ ex.get("question"); // "What is 2+2?"
225
+ ex.toDict(); // { question: "What is 2+2?", answer: "4" }
226
+ ex.toJSON(); // same as toDict()
227
+
228
+ // Non-mutating copy with overrides
229
+ const updated = ex.with({ answer: "four" });
230
+
231
+ // Filtered views
232
+ const inputOnly = ex.inputs(["question"]); // Example { question: ... }
233
+ const labelOnly = ex.labels(["question"]); // Example { answer: ... }
234
+
235
+ // Deserialize
236
+ const ex2 = Example.fromDict({ question: "Hi", answer: "Hello" });
237
+ ```
238
+
239
+ #### `Prediction`
240
+
241
+ Extends `Example` and adds `completions` for multi-output calls (`n > 1`).
242
+
243
+ ```ts
244
+ import { Prediction } from "dstsx";
245
+
246
+ const pred = new Prediction(
247
+ { answer: "42" },
248
+ [{ answer: "42" }, { answer: "forty-two" }], // completions
249
+ );
250
+
251
+ pred.get("answer"); // "42"
252
+ pred.getTyped<string>("answer"); // "42" — typed cast
253
+ pred.completions; // ReadonlyArray of all candidates
254
+ pred.toJSON(); // { answer: "42", completions: [...] }
255
+ ```
256
+
257
+ #### `Trace`
258
+
259
+ Recorded per LM call. See [History](#history) for how to read traces.
260
+
261
+ ```ts
262
+ interface Trace {
263
+ signature: Signature;
264
+ inputs: Record<string, unknown>;
265
+ outputs: Record<string, unknown>;
266
+ usage: { promptTokens: number; completionTokens: number; totalTokens: number } | null;
267
+ latencyMs: number;
268
+ timestamp: string; // ISO-8601
269
+ }
270
+ ```
271
+
272
+ ---
273
+
274
+ ### Language Model Adapters
275
+
276
+ All adapters extend `LM` and share the same `call()` interface.
277
+
278
+ #### Abstract `LM`
279
+
280
+ ```ts
281
+ abstract class LM {
282
+ readonly model: string;
283
+
284
+ // Call the LM with a string prompt or chat messages
285
+ async call(prompt: string | Message[], config?: LMCallConfig): Promise<LMResponse>;
286
+
287
+ // Counters (non-cached calls only)
288
+ get requestCount(): number;
289
+ get tokenUsage(): { promptTokens: number; completionTokens: number; totalTokens: number };
290
+
291
+ // Clear in-memory LRU response cache
292
+ clearCache(): void;
293
+ }
294
+ ```
295
+
296
+ ##### `LMCallConfig`
297
+
298
+ ```ts
299
+ interface LMCallConfig {
300
+ model?: string; // override model per call
301
+ temperature?: number; // 0–2
302
+ maxTokens?: number;
303
+ stop?: string[];
304
+ n?: number; // number of completions (default 1)
305
+ cacheKey?: string; // manual cache key override
306
+ extra?: Record<string, unknown>; // provider pass-through
307
+ }
308
+ ```
309
+
310
+ ##### `LMResponse`
311
+
312
+ ```ts
313
+ interface LMResponse {
314
+ text: string; // primary completion
315
+ texts: string[]; // all completions when n > 1
316
+ usage: TokenUsage | null;
317
+ raw: unknown; // raw provider response
318
+ }
319
+ ```
320
+
321
+ ---
322
+
323
+ #### `OpenAI`
324
+
325
+ Requires: `npm install openai`
326
+
327
+ ```ts
328
+ import { OpenAI } from "dstsx";
329
+
330
+ const lm = new OpenAI({
331
+ model: "gpt-4o", // default "gpt-4o"
332
+ apiKey: "sk-...", // or set OPENAI_API_KEY env var
333
+ baseURL: "https://...", // optional custom endpoint
334
+ maxRetries: 3,
335
+ });
336
+ ```
337
+
338
+ ---
339
+
340
+ #### `Anthropic`
341
+
342
+ Requires: `npm install @anthropic-ai/sdk`
343
+
344
+ ```ts
345
+ import { Anthropic } from "dstsx";
346
+
347
+ const lm = new Anthropic({
348
+ model: "claude-3-5-sonnet-20241022", // default
349
+ apiKey: "...", // or ANTHROPIC_API_KEY
350
+ maxRetries: 3,
351
+ });
352
+ ```
353
+
354
+ ---
355
+
356
+ #### `Cohere`
357
+
358
+ Requires: `npm install cohere-ai`
359
+
360
+ ```ts
361
+ import { Cohere } from "dstsx";
362
+
363
+ const lm = new Cohere({
364
+ model: "command-r-plus", // default
365
+ apiKey: "...", // or COHERE_API_KEY
366
+ });
367
+ ```
368
+
369
+ ---
370
+
371
+ #### `GoogleAI`
372
+
373
+ Requires: `npm install @google/generative-ai`
374
+
375
+ ```ts
376
+ import { GoogleAI } from "dstsx";
377
+
378
+ const lm = new GoogleAI({
379
+ model: "gemini-1.5-pro", // default
380
+ apiKey: "...", // or GOOGLE_API_KEY
381
+ });
382
+ ```
383
+
384
+ ---
385
+
386
+ #### `Ollama`
387
+
388
+ No extra package required — communicates with the Ollama REST API.
389
+
390
+ ```ts
391
+ import { Ollama } from "dstsx";
392
+
393
+ const lm = new Ollama({
394
+ model: "llama3", // default
395
+ baseURL: "http://localhost:11434", // default
396
+ });
397
+ ```
398
+
399
+ ---
400
+
401
+ #### `LMStudio`
402
+
403
+ No extra package required — uses LM Studio's OpenAI-compatible `/v1` endpoint.
404
+
405
+ ```ts
406
+ import { LMStudio } from "dstsx";
407
+
408
+ const lm = new LMStudio({
409
+ model: "local-model",
410
+ baseURL: "http://localhost:1234/v1", // default
411
+ });
412
+ ```
413
+
414
+ ---
415
+
416
+ #### `HuggingFace`
417
+
418
+ No extra package required — calls the HuggingFace Inference API directly.
419
+
420
+ ```ts
421
+ import { HuggingFace } from "dstsx";
422
+
423
+ const lm = new HuggingFace({
424
+ model: "mistralai/Mistral-7B-Instruct-v0.3", // default
425
+ apiKey: "...", // or HF_API_KEY
426
+ endpointURL: "https://my-dedicated-endpoint.com", // optional
427
+ });
428
+ ```
429
+
430
+ ---
431
+
432
+ #### `MockLM`
433
+
434
+ Deterministic lookup-map LM for unit testing.
435
+
436
+ ```ts
437
+ import { MockLM } from "dstsx";
438
+
439
+ const lm = new MockLM(
440
+ {
441
+ // prompt substring → response
442
+ "What is 2+2?": "answer: 4",
443
+ },
444
+ "answer: unknown", // fallback when no match (optional; throws if omitted)
445
+ );
446
+
447
+ // Add responses at runtime
448
+ lm.addResponse("What is the capital of France?", "answer: Paris");
449
+ ```
450
+
451
+ ---
452
+
453
+ ### Settings & Context
454
+
455
+ The `settings` singleton controls global defaults.
456
+
457
+ ```ts
458
+ import { settings } from "dstsx";
459
+ ```
460
+
461
+ #### `settings.configure(options)`
462
+
463
+ Merge options into global settings (existing keys are overwritten; omitted keys unchanged).
464
+
465
+ ```ts
466
+ settings.configure({
467
+ lm: new OpenAI({ model: "gpt-4o" }),
468
+ rm: new ColBERTv2("http://localhost:8893"),
469
+ lmConfig: { temperature: 0.0, maxTokens: 512 },
470
+ logLevel: "warn", // "silent" | "error" | "warn" | "info" | "debug"
471
+ cacheDir: "./.dstsx", // for future disk caching
472
+ });
473
+ ```
474
+
475
+ #### `settings.reset()`
476
+
477
+ Reset all global settings to defaults.
478
+
479
+ ```ts
480
+ settings.reset();
481
+ ```
482
+
483
+ #### `settings.inspect()`
484
+
485
+ Return a deep-frozen snapshot of currently effective settings.
486
+
487
+ ```ts
488
+ const snap = settings.inspect();
489
+ console.log(snap.lm?.model);
490
+ ```
491
+
492
+ #### `settings.context(overrides, fn)` — Per-request isolation
493
+
494
+ Run `fn` inside an `AsyncLocalStorage` scope. Concurrent requests each get their own isolated settings and never interfere.
495
+
496
+ ```ts
497
+ // In an Express/Fastify handler:
498
+ app.post("/answer", async (req, res) => {
499
+ const answer = await settings.context(
500
+ { lm: new OpenAI({ model: "gpt-4o-mini" }) },
501
+ () => program.forward({ question: req.body.question }),
502
+ );
503
+ res.json(answer.toJSON());
504
+ });
505
+ ```
506
+
507
+ #### `SettingsOptions` type
508
+
509
+ ```ts
510
+ interface SettingsOptions {
511
+ lm?: LM;
512
+ rm?: Retriever;
513
+ lmConfig?: LMCallConfig;
514
+ logLevel?: "silent" | "error" | "warn" | "info" | "debug";
515
+ cacheDir?: string;
516
+ }
517
+ ```
518
+
519
+ ---
520
+
521
+ ### Modules
522
+
523
+ All modules extend `Module` and expose a `forward()` method.
524
+
525
+ #### Abstract `Module`
526
+
527
+ ```ts
528
+ abstract class Module {
529
+ abstract forward(...args: unknown[]): Promise<Prediction>;
530
+
531
+ // Recursively list all Predict sub-modules
532
+ namedPredictors(): Array<[string, Module]>;
533
+
534
+ // Serialize/deserialize learnable parameters (demos, instructions)
535
+ dump(): Record<string, unknown>;
536
+ load(state: Record<string, unknown>): void;
537
+
538
+ // Deep-clone the module (used by optimizers)
539
+ clone(): this;
540
+ }
541
+ ```
542
+
543
+ **Saving and loading a compiled program:**
544
+
545
+ ```ts
546
+ import { writeFileSync, readFileSync } from "fs";
547
+
548
+ // Save
549
+ const state = program.dump();
550
+ writeFileSync("program.json", JSON.stringify(state, null, 2));
551
+
552
+ // Load into a fresh instance
553
+ const fresh = new MyProgram();
554
+ fresh.load(JSON.parse(readFileSync("program.json", "utf8")));
555
+ ```
556
+
557
+ ---
558
+
559
+ #### `Predict`
560
+
561
+ The fundamental module — formats a prompt and calls the LM.
562
+
563
+ ```ts
564
+ import { Predict } from "dstsx";
565
+
566
+ const qa = new Predict("question -> answer");
567
+
568
+ // Or with a full Signature object
569
+ const qa2 = new Predict(Signature.from("context, question -> answer"));
570
+
571
+ // Forward
572
+ const result = await qa.forward({ question: "What is 9 × 7?" });
573
+ console.log(result.get("answer")); // "63"
574
+ ```
575
+
576
+ **Learnable parameters** (mutated by optimizers):
577
+
578
+ ```ts
579
+ qa.demos // Example[] — few-shot demonstrations
580
+ qa.instructions // string | undefined — system instruction override
581
+ ```
582
+
583
+ **Multiple completions (`n > 1`):**
584
+
585
+ ```ts
586
+ settings.configure({ lmConfig: { n: 5 } });
587
+ const result = await qa.forward({ question: "Name a color." });
588
+ console.log(result.completions); // 5 candidate answers
589
+ ```
590
+
591
+ ---
592
+
593
+ #### `ChainOfThought`
594
+
595
+ Prepends a hidden `rationale` output field so the LM reasons step-by-step before answering.
596
+
597
+ ```ts
598
+ import { ChainOfThought } from "dstsx";
599
+
600
+ const cot = new ChainOfThought("question -> answer");
601
+
602
+ // Optional: customize the rationale description
603
+ const cot2 = new ChainOfThought("question -> answer", {
604
+ rationaleDescription: "Think aloud to solve the problem",
605
+ });
606
+
607
+ const result = await cot.forward({ question: "If Alice has 3 apples and gets 5 more, how many does she have?" });
608
+ console.log(result.get("answer")); // "8" — rationale is internal
609
+ ```
610
+
611
+ ---
612
+
613
+ #### `ChainOfThoughtWithHint`
614
+
615
+ Extends `ChainOfThought` with an optional `hint` input.
616
+
617
+ ```ts
618
+ import { ChainOfThoughtWithHint } from "dstsx";
619
+
620
+ const cot = new ChainOfThoughtWithHint("question -> answer");
621
+
622
+ const result = await cot.forward({
623
+ question: "What is the chemical formula for water?",
624
+ hint: "It involves hydrogen and oxygen.",
625
+ });
626
+ ```
627
+
628
+ ---
629
+
630
+ #### `MultiChainComparison`
631
+
632
+ Runs a signature `M` times and picks the best completion via a final aggregation call.
633
+
634
+ ```ts
635
+ import { MultiChainComparison } from "dstsx";
636
+
637
+ const mcc = new MultiChainComparison("question -> answer", /* M= */ 3);
638
+ const result = await mcc.forward({ question: "What is 7 × 8?" });
639
+ ```
640
+
641
+ ---
642
+
643
+ #### `ReAct`
644
+
645
+ Reasoning + Acting loop (Yao et al., 2022). Alternates Thought → Action → Observation until the LM emits `Finish[answer]` or `maxIter` is reached.
646
+
647
+ ```ts
648
+ import { ReAct, type Tool } from "dstsx";
649
+
650
+ const searchTool: Tool = {
651
+ name: "search",
652
+ description: "Search the web for current information",
653
+ fn: async (query: string) => {
654
+ // Your real implementation here
655
+ return `Search results for: ${query}`;
656
+ },
657
+ };
658
+
659
+ const agent = new ReAct(
660
+ "question -> answer",
661
+ [searchTool],
662
+ /* maxIter= */ 5,
663
+ );
664
+
665
+ const result = await agent.forward({ question: "Who won the 2024 US election?" });
666
+ console.log(result.get("answer"));
667
+ console.log(result.get("trajectory")); // full thought/action/observation log
668
+ ```
669
+
670
+ **`Tool` interface:**
671
+
672
+ ```ts
673
+ interface Tool {
674
+ name: string;
675
+ description: string;
676
+ fn: (args: string) => Promise<string>;
677
+ }
678
+ ```
679
+
680
+ ---
681
+
682
+ #### `ProgramOfThought`
683
+
684
+ Generates JavaScript code, executes it in a `new Function()` context, self-corrects on errors, and returns the result.
685
+
686
+ > ⚠️ **Security**: Code runs in the current process. Do NOT use with untrusted user inputs in production without an additional sandboxing layer (e.g. a Worker thread).
687
+
688
+ ```ts
689
+ import { ProgramOfThought } from "dstsx";
690
+
691
+ const pot = new ProgramOfThought(
692
+ "question -> answer",
693
+ /* maxAttempts= */ 3,
694
+ /* timeoutMs= */ 5_000,
695
+ );
696
+
697
+ const result = await pot.forward({ question: "What is the 10th Fibonacci number?" });
698
+ console.log(result.get("answer")); // "55"
699
+ console.log(result.get("code")); // the generated JS code
700
+ ```
701
+
702
+ ---
703
+
704
+ #### `Retrieve`
705
+
706
+ Calls the globally configured retriever and returns `passages`.
707
+
708
+ ```ts
709
+ import { Retrieve, ColBERTv2, settings } from "dstsx";
710
+
711
+ settings.configure({ rm: new ColBERTv2("http://localhost:8893") });
712
+
713
+ const retrieve = new Retrieve(/* k= */ 3);
714
+ const result = await retrieve.forward("What is DSPy?");
715
+
716
+ const passages: string[] = result.get("passages") as string[];
717
+ ```
718
+
719
+ ---
720
+
721
+ #### `Retry`
722
+
723
+ Wraps any module and retries on `AssertionError` (thrown by `Assert()`), feeding the error message back as `feedback`.
724
+
725
+ ```ts
726
+ import { Retry, Assert, Predict } from "dstsx";
727
+
728
+ const qa = new Predict("question, feedback? -> answer");
729
+
730
+ const retrying = new Retry(qa, /* maxAttempts= */ 3);
731
+
732
+ const result = await retrying.forward({
733
+ question: "Give a one-word answer: what color is the sky?",
734
+ });
735
+
736
+ // Use Assert inside a custom module to trigger retry
737
+ class CheckedQA extends Module {
738
+ predict = new Predict("question -> answer");
739
+ async forward(inputs: { question: string }) {
740
+ const result = await this.predict.forward(inputs);
741
+ Assert(
742
+ String(result.get("answer")).length > 0,
743
+ "Answer must not be empty"
744
+ );
745
+ return result;
746
+ }
747
+ }
748
+ ```
749
+
750
+ ---
751
+
752
+ #### `BestOfN`
753
+
754
+ Runs `N` copies of a module in parallel and selects the best via `reduceFunc` (defaults to first result).
755
+
756
+ ```ts
757
+ import { BestOfN, Predict } from "dstsx";
758
+
759
+ const qa = new Predict("question -> answer");
760
+ const best = new BestOfN(qa, /* N= */ 5, (predictions) => {
761
+ // Pick the longest answer as a proxy for quality
762
+ return predictions.reduce((a, b) =>
763
+ String(b.get("answer")).length > String(a.get("answer")).length ? b : a
764
+ );
765
+ });
766
+
767
+ const result = await best.forward({ question: "Explain gravity." });
768
+ ```
769
+
770
+ ---
771
+
772
+ #### `Ensemble`
773
+
774
+ Combines multiple pre-built module instances via a reduce function.
775
+
776
+ ```ts
777
+ import { Ensemble, ChainOfThought } from "dstsx";
778
+
779
+ const m1 = new ChainOfThought("question -> answer");
780
+ const m2 = new ChainOfThought("question -> answer");
781
+
782
+ const ensemble = new Ensemble(
783
+ [m1, m2],
784
+ (predictions) => predictions[0]!, // custom vote/merge logic
785
+ );
786
+
787
+ const result = await ensemble.forward({ question: "Is TypeScript better than JavaScript?" });
788
+ ```
789
+
790
+ ---
791
+
792
+ ### Retrievers
793
+
794
+ All retrievers extend `Retriever` and implement `retrieve(query, k)`.
795
+
796
+ #### Abstract `Retriever`
797
+
798
+ ```ts
799
+ abstract class Retriever {
800
+ abstract retrieve(query: string, k: number): Promise<string[]>;
801
+ }
802
+ ```
803
+
804
+ #### `ColBERTv2`
805
+
806
+ ```ts
807
+ import { ColBERTv2 } from "dstsx";
808
+
809
+ const rm = new ColBERTv2("http://localhost:8893");
810
+ // or with options:
811
+ const rm2 = new ColBERTv2({ url: "http://localhost:8893" });
812
+
813
+ const passages = await rm.retrieve("What is photosynthesis?", 3);
814
+ ```
815
+
816
+ #### `PineconeRM`
817
+
818
+ Requires: `npm install @pinecone-database/pinecone`
819
+
820
+ ```ts
821
+ import { PineconeRM } from "dstsx";
822
+
823
+ const rm = new PineconeRM({
824
+ indexName: "my-index",
825
+ apiKey: "...", // or PINECONE_API_KEY
826
+ namespace: "default",
827
+ embeddingFn: async (text) => myEmbedModel.embed(text),
828
+ });
829
+ ```
830
+
831
+ #### `ChromadbRM`
832
+
833
+ Requires: `npm install chromadb`
834
+
835
+ ```ts
836
+ import { ChromadbRM } from "dstsx";
837
+
838
+ const rm = new ChromadbRM({
839
+ collectionName: "my-collection",
840
+ url: "http://localhost:8000", // default
841
+ embeddingFn: async (texts) => myEmbedModel.embedBatch(texts),
842
+ });
843
+ ```
844
+
845
+ #### `QdrantRM`
846
+
847
+ Requires: `npm install @qdrant/js-client-rest`
848
+
849
+ ```ts
850
+ import { QdrantRM } from "dstsx";
851
+
852
+ const rm = new QdrantRM({
853
+ url: "http://localhost:6333",
854
+ collectionName: "my-collection",
855
+ embeddingFn: async (text) => myEmbedModel.embed(text),
856
+ });
857
+ ```
858
+
859
+ #### `WeaviateRM`
860
+
861
+ Requires: `npm install weaviate-client`
862
+
863
+ ```ts
864
+ import { WeaviateRM } from "dstsx";
865
+
866
+ const rm = new WeaviateRM({
867
+ url: "http://localhost:8080",
868
+ className: "Document",
869
+ textField: "content",
870
+ embeddingFn: async (text) => myEmbedModel.embed(text),
871
+ });
872
+ ```
873
+
874
+ #### `FaissRM`
875
+
876
+ Requires: `npm install faiss-node` (optional peer dep)
877
+
878
+ ```ts
879
+ import { FaissRM } from "dstsx";
880
+
881
+ const rm = new FaissRM({
882
+ passages: ["passage 1", "passage 2"],
883
+ embeddingFn: async (text) => myEmbedModel.embed(text),
884
+ });
885
+ ```
886
+
887
+ #### `YouRM`
888
+
889
+ ```ts
890
+ import { YouRM } from "dstsx";
891
+
892
+ const rm = new YouRM({
893
+ apiKey: "...", // or YDC_API_KEY
894
+ k: 3,
895
+ });
896
+ ```
897
+
898
+ #### `MockRetriever`
899
+
900
+ For unit testing.
901
+
902
+ ```ts
903
+ import { MockRetriever } from "dstsx";
904
+
905
+ const rm = new MockRetriever([
906
+ "The capital of France is Paris.",
907
+ "Paris is located in northern France.",
908
+ "France is a country in Western Europe.",
909
+ ]);
910
+
911
+ const passages = await rm.retrieve("capital of France", 2);
912
+ ```
913
+
914
+ ---
915
+
916
+ ### Optimizers
917
+
918
+ Optimizers automatically tune a module's few-shot `demos` and/or `instructions`. All optimizers implement:
919
+
920
+ ```ts
921
+ abstract class Optimizer {
922
+ abstract compile(
923
+ student: Module,
924
+ trainset: Example[],
925
+ metric: Metric,
926
+ ): Promise<Module>;
927
+ }
928
+ ```
929
+
930
+ - The returned module is a **new clone**; the original `student` is never mutated.
931
+ - Pass a `valset` where supported to evaluate on held-out data.
932
+
933
+ ---
934
+
935
+ #### `LabeledFewShot`
936
+
937
+ Directly assigns labeled examples as `demos` on every `Predict` sub-module (no LM calls).
938
+
939
+ ```ts
940
+ import { LabeledFewShot } from "dstsx";
941
+
942
+ const optimizer = new LabeledFewShot(/* k= */ 16);
943
+ const optimized = await optimizer.compile(program, trainset, metric);
944
+ ```
945
+
946
+ ---
947
+
948
+ #### `BootstrapFewShot`
949
+
950
+ Runs the student (or an optional `teacher`) on `trainset`, collects successful traces via `metric`, and uses them as `demos`.
951
+
952
+ ```ts
953
+ import { BootstrapFewShot } from "dstsx";
954
+
955
+ const optimizer = new BootstrapFewShot({
956
+ maxBootstrappedDemos: 4, // max demos collected per predictor
957
+ maxLabeledDemos: 16, // max labeled fallback demos
958
+ teacher: expertProgram, // optional; defaults to student
959
+ });
960
+
961
+ const optimized = await optimizer.compile(program, trainset, exactMatch("answer"));
962
+ ```
963
+
964
+ ---
965
+
966
+ #### `BootstrapFewShotWithRandomSearch`
967
+
968
+ Extends `BootstrapFewShot` — tries `numCandidatePrograms` random demo subsets and selects the best by validation score.
969
+
970
+ ```ts
971
+ import { BootstrapFewShotWithRandomSearch } from "dstsx";
972
+
973
+ const optimizer = new BootstrapFewShotWithRandomSearch({
974
+ maxBootstrappedDemos: 4,
975
+ numCandidatePrograms: 8, // number of random subsets to evaluate
976
+ valset: valExamples, // optional held-out set
977
+ });
978
+
979
+ const optimized = await optimizer.compile(program, trainset, metric);
980
+ ```
981
+
982
+ ---
983
+
984
+ #### `COPRO` (Collaborative Prompt Optimizer)
985
+
986
+ Uses the LM to propose instruction improvements for each `Predict` sub-module and selects the best combination by metric score.
987
+
988
+ ```ts
989
+ import { COPRO } from "dstsx";
990
+
991
+ const optimizer = new COPRO({
992
+ breadth: 5, // instruction candidates per predictor per round
993
+ depth: 3, // refinement rounds
994
+ });
995
+
996
+ const optimized = await optimizer.compile(program, trainset, metric);
997
+ ```
998
+
999
+ ---
1000
+
1001
+ #### `MIPRO` (Multi-stage Instruction Prompt Optimizer)
1002
+
1003
+ Combines COPRO-style instruction proposals with `BootstrapFewShotWithRandomSearch` to jointly optimize instructions _and_ demonstrations.
1004
+
1005
+ ```ts
1006
+ import { MIPRO } from "dstsx";
1007
+
1008
+ const optimizer = new MIPRO({
1009
+ numCandidates: 5, // instruction candidates per predictor
1010
+ initTemperature: 0.9,
1011
+ numCandidatePrograms: 8, // demo subsets to evaluate
1012
+ verbose: true,
1013
+ });
1014
+
1015
+ const optimized = await optimizer.compile(program, trainset, metric);
1016
+ ```
1017
+
1018
+ ---
1019
+
1020
+ #### `KNNFewShot`
1021
+
1022
+ Selects demonstrations **at inference time** using k-nearest-neighbour search over the training set embeddings (dynamic few-shot).
1023
+
1024
+ ```ts
1025
+ import { KNNFewShot } from "dstsx";
1026
+
1027
+ const optimizer = new KNNFewShot({
1028
+ k: 3,
1029
+ embeddingFn: async (text) => myEmbedModel.embed(text), // required
1030
+ keyField: "question", // which field to embed (default: all fields joined)
1031
+ });
1032
+
1033
+ const optimized = await optimizer.compile(program, trainset, metric);
1034
+ // At inference time, each forward() call auto-selects the 3 most similar demos
1035
+ ```
1036
+
1037
+ ---
1038
+
1039
+ #### `EnsembleOptimizer`
1040
+
1041
+ Wraps a program with an optional reduce function. Primarily useful for building multi-program ensembles.
1042
+
1043
+ ```ts
1044
+ import { EnsembleOptimizer } from "dstsx";
1045
+
1046
+ const optimizer = new EnsembleOptimizer({
1047
+ reduceFunc: (predictions) => predictions[0]!,
1048
+ });
1049
+
1050
+ const wrapped = await optimizer.compile(program, trainset, metric);
1051
+ ```
1052
+
1053
+ ---
1054
+
1055
+ ### Evaluation
1056
+
1057
+ #### `evaluate(program, examples, metric, options?)`
1058
+
1059
+ Run `program` on every example and aggregate scores.
1060
+
1061
+ ```ts
1062
+ import { evaluate, exactMatch } from "dstsx";
1063
+
1064
+ const result = await evaluate(
1065
+ program,
1066
+ devset,
1067
+ exactMatch("answer"), // built-in metric
1068
+ {
1069
+ numThreads: 4, // parallel evaluation (default: 1)
1070
+ displayProgress: true, // log progress to console
1071
+ },
1072
+ );
1073
+
1074
+ console.log(`Score: ${(result.score * 100).toFixed(1)}%`);
1075
+ console.log(`Passed: ${result.numPassed}/${result.total}`);
1076
+ ```
1077
+
1078
+ ##### `EvaluationResult`
1079
+
1080
+ ```ts
1081
+ interface EvaluationResult {
1082
+ score: number; // average metric score (0–1)
1083
+ numPassed: number;
1084
+ total: number;
1085
+ results: ExampleResult[]; // per-example breakdown
1086
+ }
1087
+
1088
+ interface ExampleResult {
1089
+ example: Example;
1090
+ prediction: Prediction;
1091
+ score: number;
1092
+ passed: boolean;
1093
+ }
1094
+ ```
1095
+
1096
+ ---
1097
+
1098
+ #### Built-in Metrics
1099
+
1100
+ All metrics implement `Metric`:
1101
+
1102
+ ```ts
1103
+ type Metric = (
1104
+ example: Example,
1105
+ prediction: Prediction,
1106
+ trace?: Trace[],
1107
+ ) => number | boolean;
1108
+ ```
1109
+
1110
+ | Factory | Description |
1111
+ |---|---|
1112
+ | `exactMatch(field?, caseSensitive?)` | 1 if prediction exactly matches example (case-insensitive by default) |
1113
+ | `f1(field?)` | Token-level F1 (word overlap), useful for QA |
1114
+ | `passAtK(innerMetric, k)` | 1 if any of the top-k completions pass `innerMetric` |
1115
+ | `bleu(field?)` | Simplified BLEU (1-gram + 2-gram precision) |
1116
+ | `rouge(field?)` | ROUGE-L (LCS-based F1) |
1117
+
1118
+ ```ts
1119
+ import { exactMatch, f1, passAtK, bleu, rouge } from "dstsx";
1120
+
1121
+ // Exact match on "answer" field (default)
1122
+ const em = exactMatch();
1123
+
1124
+ // Case-sensitive exact match on a custom field
1125
+ const em2 = exactMatch("label", true);
1126
+
1127
+ // Token F1
1128
+ const f1Metric = f1("answer");
1129
+
1130
+ // Pass if any of the 5 completions give exact match
1131
+ const p5 = passAtK(exactMatch(), 5);
1132
+
1133
+ // BLEU / ROUGE
1134
+ const bleuMetric = bleu("answer");
1135
+ const rougeMetric = rouge("answer");
1136
+ ```
1137
+
1138
+ ---
1139
+
1140
+ ### Assertions & Suggestions
1141
+
1142
+ #### `Assert(condition, message?)`
1143
+
1144
+ Throws `AssertionError` if `condition` is falsy. Caught and retried by `Retry`.
1145
+
1146
+ ```ts
1147
+ import { Assert } from "dstsx";
1148
+
1149
+ Assert(result.get("answer") !== "", "Answer must not be empty");
1150
+ Assert(typeof result.get("score") === "number", "Score must be a number");
1151
+ ```
1152
+
1153
+ #### `Suggest(condition, message?)`
1154
+
1155
+ Logs a `console.warn` if `condition` is falsy but does **not** throw — the pipeline continues.
1156
+
1157
+ ```ts
1158
+ import { Suggest } from "dstsx";
1159
+
1160
+ Suggest(result.get("confidence") === "high", "Low confidence in answer");
1161
+ ```
1162
+
1163
+ #### `AssertionError`
1164
+
1165
+ The typed error class thrown by `Assert`. Caught by `Retry`.
1166
+
1167
+ ```ts
1168
+ import { AssertionError } from "dstsx";
1169
+
1170
+ try {
1171
+ await program.forward(inputs);
1172
+ } catch (err) {
1173
+ if (err instanceof AssertionError) {
1174
+ console.warn("Assertion failed:", err.message);
1175
+ }
1176
+ }
1177
+ ```
1178
+
1179
+ ---
1180
+
1181
+ ## V2 APIs
1182
+
1183
+ The following features are implemented in DSTsx v2.
1184
+
1185
+ ### `TypedPredictor` & `TypedChainOfThought`
1186
+
1187
+ Structured JSON output with optional schema validation. Works without any extra
1188
+ dependencies — pass a [Zod](https://github.com/colinhacks/zod) schema for
1189
+ runtime validation.
1190
+
1191
+ #### `TypedPrediction<T>`
1192
+
1193
+ Extends `Prediction` and adds a `.typed` field with the validated/parsed type.
1194
+
1195
+ ```ts
1196
+ import { TypedPredictor } from "dstsx";
1197
+
1198
+ // Without schema — output is parsed as plain JSON
1199
+ const qa = new TypedPredictor("question -> answer");
1200
+ const result = await qa.forward({ question: "What is π?" });
1201
+ const typed = result.typed as { answer: string };
1202
+ console.log(typed.answer);
1203
+ ```
1204
+
1205
+ With a Zod schema (`npm install zod` first):
1206
+
1207
+ ```ts
1208
+ import { z } from "zod";
1209
+ import { TypedPredictor, TypedChainOfThought } from "dstsx";
1210
+
1211
+ const AnswerSchema = z.object({
1212
+ answer: z.string(),
1213
+ confidence: z.number().min(0).max(1),
1214
+ sources: z.array(z.string()).optional(),
1215
+ });
1216
+
1217
+ const qa = new TypedPredictor("question -> answer", AnswerSchema, { maxRetries: 3 });
1218
+ const result = await qa.forward({ question: "What is 2 + 2?" });
1219
+
1220
+ // result.typed is z.infer<typeof AnswerSchema>
1221
+ console.log(result.typed.confidence); // 0.98 (number, validated)
1222
+ ```
1223
+
1224
+ `TypedChainOfThought` adds a hidden `rationale` step before producing the JSON:
1225
+
1226
+ ```ts
1227
+ const cot = new TypedChainOfThought("question -> answer", AnswerSchema);
1228
+ const result = await cot.forward({ question: "Explain gravity briefly." });
1229
+ ```
1230
+
1231
+ **Constructor options:**
1232
+
1233
+ ```ts
1234
+ new TypedPredictor(signature, schema?, { maxRetries?: number })
1235
+ // maxRetries: how many times to retry on parse/schema failure (default: 3)
1236
+ ```
1237
+
1238
+ ---
1239
+
1240
+ ### `Parallel` Module
1241
+
1242
+ Runs multiple modules concurrently with `Promise.all` and returns all results.
1243
+
1244
+ ```ts
1245
+ import { Parallel, Predict, ChainOfThought } from "dstsx";
1246
+
1247
+ const pipeline = new Parallel([
1248
+ new Predict("question -> answer"),
1249
+ new ChainOfThought("question -> answer"),
1250
+ ], { timeoutMs: 10_000 }); // optional per-module timeout
1251
+
1252
+ // run() returns Prediction[] — one per module
1253
+ const [directAnswer, cotAnswer] = await pipeline.run({ question: "What is π?" });
1254
+
1255
+ // forward() returns the first prediction (for Module interface compat)
1256
+ const first = await pipeline.forward({ question: "What is π?" });
1257
+ ```
1258
+
1259
+ **Constructor:**
1260
+
1261
+ ```ts
1262
+ new Parallel(modules: Module[], options?: { timeoutMs?: number })
1263
+ ```
1264
+
1265
+ | Method | Returns | Description |
1266
+ |---|---|---|
1267
+ | `run(...args)` | `Promise<Prediction[]>` | All module outputs in order |
1268
+ | `forward(...args)` | `Promise<Prediction>` | First module output (Module compat) |
1269
+
1270
+ ---
1271
+
1272
+ ### `Refine` Module
1273
+
1274
+ Self-critique / iterative refinement loop. After each inner module run, a built-in
1275
+ critic predictor evaluates the output and feeds improvement suggestions back.
1276
+
1277
+ ```ts
1278
+ import { Refine, Predict } from "dstsx";
1279
+
1280
+ const writer = new Predict("topic, feedback? -> essay");
1281
+
1282
+ const refined = new Refine(writer, {
1283
+ maxRefinements: 2,
1284
+ feedbackField: "feedback", // injected field name for critique
1285
+ stopCondition: (pred) =>
1286
+ String(pred.get("essay")).length > 500, // stop early if long enough
1287
+ });
1288
+
1289
+ const result = await refined.forward({ topic: "Climate change" });
1290
+ console.log(result.get("essay"));
1291
+ ```
1292
+
1293
+ **Constructor:**
1294
+
1295
+ ```ts
1296
+ new Refine(inner: Module, options?: {
1297
+ maxRefinements?: number; // default: 2
1298
+ feedbackField?: string; // default: "feedback"
1299
+ stopCondition?: (p: Prediction) => boolean; // optional early-exit check
1300
+ })
1301
+ ```
1302
+
1303
+ The critic calls `Predict("output -> critique, is_satisfactory")`.
1304
+ If `is_satisfactory` is `"yes"` or `"true"`, refinement stops early.
1305
+
1306
+ ---
1307
+
1308
+ ### `majority()` Helper
1309
+
1310
+ Votes across multiple `Prediction` instances by the most common value for a given
1311
+ field. Useful as a `reduceFunc` in `BestOfN` and `Ensemble`.
1312
+
1313
+ ```ts
1314
+ import { majority, BestOfN, Predict } from "dstsx";
1315
+
1316
+ const qa = new Predict("question -> answer");
1317
+
1318
+ // Run 5 times and pick the most common answer
1319
+ const voted = new BestOfN(qa, 5, majority("answer"));
1320
+ const result = await voted.forward({ question: "What color is the sky?" });
1321
+ console.log(result.get("answer")); // most frequently returned answer
1322
+ ```
1323
+
1324
+ ```ts
1325
+ // Standalone usage
1326
+ import { majority } from "dstsx";
1327
+
1328
+ const reducer = majority("answer");
1329
+ const best = reducer([pred1, pred2, pred3]); // Prediction with the most common "answer"
1330
+ ```
1331
+
1332
+ ---
1333
+
1334
+ ### `BootstrapFewShotWithOptuna`
1335
+
1336
+ Extends `BootstrapFewShot` with a pure-TypeScript TPE (Tree-structured Parzen
1337
+ Estimator) that searches demo subsets across `numTrials` iterations, learning
1338
+ from past trial outcomes to find the best configuration — no external
1339
+ dependencies required.
1340
+
1341
+ ```ts
1342
+ import { BootstrapFewShotWithOptuna } from "dstsx";
1343
+
1344
+ const optimizer = new BootstrapFewShotWithOptuna({
1345
+ maxBootstrappedDemos: 4,
1346
+ numTrials: 20, // number of TPE search trials
1347
+ valset: valExamples, // optional held-out validation set
1348
+ });
1349
+
1350
+ const optimized = await optimizer.compile(program, trainset, metric);
1351
+ ```
1352
+
1353
+ **How it works:** First runs `BootstrapFewShot` to collect candidate demos. Then
1354
+ runs `numTrials` iterations where each trial samples a demo subset using TPE:
1355
+ the top 25 % of past trials form the "good" pool, sampled with 70 % probability,
1356
+ biased mutations towards the best configurations found so far.
1357
+
1358
+ ---
1359
+
1360
+ ### Disk-Persistent LM Cache
1361
+
1362
+ LM adapters now accept a `cacheDir` option. Responses are persisted as JSON
1363
+ files named by a SHA-256 hash of the prompt, surviving process restarts.
1364
+
1365
+ ```ts
1366
+ import { OpenAI, MockLM } from "dstsx";
1367
+
1368
+ // Any LM adapter — just pass cacheDir
1369
+ const lm = new OpenAI({
1370
+ model: "gpt-4o",
1371
+ cacheDir: "./.dstsx-cache", // optional disk persistence
1372
+ });
1373
+
1374
+ // Or with MockLM for testing disk cache behavior
1375
+ const mockLm = new MockLM({ "q": "a" }, undefined, { cacheDir: "/tmp/test-cache" });
1376
+ ```
1377
+
1378
+ The disk cache is checked **after** the in-memory LRU cache. On a hit the
1379
+ response is also written back into the in-memory cache. TTL and max-size
1380
+ eviction apply to both layers.
1381
+
1382
+ `DiskCache` is also exported for custom use:
1383
+
1384
+ ```ts
1385
+ import { DiskCache } from "dstsx";
1386
+
1387
+ const cache = new DiskCache(
1388
+ "./.dstsx-cache", // directory (created automatically)
1389
+ 500, // maxSize (files); default 500
1390
+ 3_600_000, // ttlMs; default undefined (no TTL)
1391
+ );
1392
+
1393
+ cache.set("myKey", lmResponse);
1394
+ const cached = cache.get("myKey");
1395
+ cache.clear(); // delete all cache files
1396
+ ```
1397
+
1398
+ ---
1399
+
1400
+ ### MCP Integration
1401
+
1402
+ DSTsx integrates with the [Model Context Protocol](https://modelcontextprotocol.io/)
1403
+ (MCP) in two directions:
1404
+
1405
+ 1. **Use MCP servers as tools** inside ReAct agents (`MCPToolAdapter`)
1406
+ 2. **Expose DSTsx modules as MCP tools** (`DSTsxMCPServer`)
1407
+
1408
+ Optional peer dependency: `npm install @modelcontextprotocol/sdk`
1409
+
1410
+ ---
1411
+
1412
+ #### `MCPToolAdapter` — consume MCP servers in ReAct
1413
+
1414
+ Wraps the tools from an MCP server as DSTsx `Tool` objects for use with `ReAct`.
1415
+
1416
+ ```ts
1417
+ import { MCPToolAdapter, ReAct } from "dstsx";
1418
+
1419
+ const adapter = new MCPToolAdapter({
1420
+ // Test mode: supply tool definitions + call handler without a live server
1421
+ tools: [
1422
+ {
1423
+ name: "weather",
1424
+ description: "Get current weather for a city",
1425
+ inputSchema: {
1426
+ type: "object",
1427
+ properties: { city: { type: "string" } },
1428
+ required: ["city"],
1429
+ },
1430
+ },
1431
+ ],
1432
+ callHandler: async (name, args) => {
1433
+ if (name === "weather") return `Sunny in ${args["city"] as string}`;
1434
+ throw new Error(`Unknown tool: ${name}`);
1435
+ },
1436
+ });
1437
+
1438
+ const tools = await adapter.getTools();
1439
+ const agent = new ReAct("question -> answer", tools, 5);
1440
+ const result = await agent.forward({ question: "What is the weather in Paris?" });
1441
+ ```
1442
+
1443
+ When `@modelcontextprotocol/sdk` is installed, a live SSE/stdio connection can
1444
+ be established by setting `serverUrl` (full live-connection implementation is in
1445
+ the [v2 roadmap](./V2_ROADMAP.md#live-mcp-connection)).
1446
+
1447
+ ---
1448
+
1449
+ #### `DSTsxMCPServer` — expose DSTsx modules as MCP tools
1450
+
1451
+ Register any DSTsx module and serve it as an MCP-compatible tool.
1452
+
1453
+ ```ts
1454
+ import { DSTsxMCPServer, ChainOfThought, settings, OpenAI } from "dstsx";
1455
+
1456
+ settings.configure({ lm: new OpenAI({ model: "gpt-4o" }) });
1457
+
1458
+ const qa = new ChainOfThought("context, question -> answer");
1459
+
1460
+ const server = new DSTsxMCPServer();
1461
+ server.registerModule(
1462
+ "qa", // tool name
1463
+ "Answer questions using chain-of-thought reasoning",
1464
+ qa,
1465
+ ["context", "question"], // input field names
1466
+ );
1467
+
1468
+ // List tools (for MCP handshake)
1469
+ const toolDefs = server.getToolDefinitions();
1470
+ /*
1471
+ [{
1472
+ name: "qa",
1473
+ description: "...",
1474
+ inputSchema: { type: "object", properties: { context: { type: "string" }, ... } },
1475
+ }]
1476
+ */
1477
+
1478
+ // Handle a tool call
1479
+ const result = await server.callTool("qa", {
1480
+ context: "Paris is the capital of France.",
1481
+ question: "What is the capital of France?",
1482
+ });
1483
+ // result is the Prediction.toJSON() object
1484
+
1485
+ // With @modelcontextprotocol/sdk installed, launch a stdio MCP server:
1486
+ // await server.createStdioServer();
1487
+ ```
1488
+
1489
+ **MCPTool type:**
1490
+
1491
+ ```ts
1492
+ interface MCPTool {
1493
+ name: string;
1494
+ description: string;
1495
+ inputSchema: {
1496
+ type: "object";
1497
+ properties: Record<string, { type: string; description?: string }>;
1498
+ required?: string[];
1499
+ };
1500
+ handler: (inputs: Record<string, unknown>) => Promise<unknown>;
1501
+ }
1502
+ ```
1503
+
1504
+ **`DSTsxMCPServer` methods:**
1505
+
1506
+ | Method | Description |
1507
+ |---|---|
1508
+ | `registerModule(name, desc, module, fields)` | Register a module as an MCP tool |
1509
+ | `getToolDefinitions()` | Return all registered `MCPTool[]` |
1510
+ | `callTool(name, inputs)` | Invoke a registered tool by name |
1511
+ | `createStdioServer()` | Start an MCP stdio server (requires SDK) |
1512
+
1513
+ ---
1514
+
1515
+ ### LM Streaming
1516
+
1517
+ Stream LM responses token by token using `AsyncGenerator`. All adapters provide
1518
+ a default fallback that returns the full response as a single chunk. Real
1519
+ streaming is implemented for `OpenAI` and `Anthropic`.
1520
+
1521
+ ```ts
1522
+ import { settings, OpenAI, Predict } from "dstsx";
1523
+
1524
+ settings.configure({ lm: new OpenAI({ model: "gpt-4o" }) });
1525
+
1526
+ const qa = new Predict("question -> answer");
1527
+
1528
+ // Stream via Predict
1529
+ for await (const chunk of qa.stream({ question: "Tell me a story." })) {
1530
+ process.stdout.write(chunk.delta);
1531
+ if (chunk.done) break;
1532
+ }
1533
+
1534
+ // Stream directly from LM
1535
+ const lm = new OpenAI({ model: "gpt-4o" });
1536
+ for await (const chunk of lm.stream("What is TypeScript?")) {
1537
+ process.stdout.write(chunk.delta);
1538
+ }
1539
+ ```
1540
+
1541
+ **`StreamChunk` type:**
1542
+
1543
+ ```ts
1544
+ interface StreamChunk {
1545
+ delta: string; // Incremental text
1546
+ done: boolean; // True on the final chunk
1547
+ raw: unknown; // Raw provider chunk
1548
+ }
1549
+ ```
1550
+
1551
+ | Method | Available on | Description |
1552
+ |---|---|---|
1553
+ | `lm.stream(prompt, config?)` | `LM` (all adapters) | Stream from LM (fallback on unsupported) |
1554
+ | `predict.stream(inputs)` | `Predict` | Stream from a Predict module |
1555
+
1556
+ ---
1557
+
1558
+ ### NativeReAct
1559
+
1560
+ A `ReAct` variant that uses provider-native function/tool calling (OpenAI tools
1561
+ API, Anthropic tool_use) instead of text-based action parsing. Falls back to
1562
+ text format for adapters that don't support native calling.
1563
+
1564
+ ```ts
1565
+ import { NativeReAct, settings, OpenAI } from "dstsx";
1566
+ import type { Tool } from "dstsx";
1567
+
1568
+ settings.configure({ lm: new OpenAI({ model: "gpt-4o" }) });
1569
+
1570
+ const tools: Tool[] = [
1571
+ {
1572
+ name: "search",
1573
+ description: "Search the web for information",
1574
+ fn: async (args) => {
1575
+ const { query } = JSON.parse(args) as { query: string };
1576
+ return `Results for: ${query}`;
1577
+ },
1578
+ },
1579
+ ];
1580
+
1581
+ const agent = new NativeReAct("question -> answer", tools, 5);
1582
+ const result = await agent.forward({ question: "What is the capital of France?" });
1583
+ console.log(result.get("answer")); // "Paris"
1584
+ ```
1585
+
1586
+ **Constructor:**
1587
+
1588
+ ```ts
1589
+ new NativeReAct(
1590
+ signature: string,
1591
+ tools: Tool[],
1592
+ maxIter?: number, // default: 5
1593
+ )
1594
+ ```
1595
+
1596
+ NativeReAct wraps tool calls using the OpenAI `tools` format (an array of
1597
+ `{ type: "function", function: { name, description, parameters } }` objects),
1598
+ which is also accepted by other compatible providers.
1599
+
1600
+ ---
1601
+
1602
+ ### Image — Multi-modal Support
1603
+
1604
+ The `Image` primitive enables passing images to vision-capable LMs as field
1605
+ values in any `Predict` or `TypedPredictor` call.
1606
+
1607
+ ```ts
1608
+ import { Image, Predict, settings, OpenAI } from "dstsx";
1609
+
1610
+ settings.configure({ lm: new OpenAI({ model: "gpt-4o" }) });
1611
+
1612
+ const captioner = new Predict("image, question -> caption");
1613
+
1614
+ // From a URL (lazy — no download at construction time)
1615
+ const img1 = Image.fromURL("https://example.com/photo.jpg");
1616
+ const result1 = await captioner.forward({ image: img1, question: "Describe this image." });
1617
+
1618
+ // From base64 data
1619
+ const img2 = Image.fromBase64(base64String, "image/png");
1620
+
1621
+ // From a local file (read synchronously)
1622
+ const img3 = Image.fromFile("./photo.jpg");
1623
+ ```
1624
+
1625
+ **Static factory methods:**
1626
+
1627
+ | Method | Description |
1628
+ |---|---|
1629
+ | `Image.fromURL(url)` | Image at a remote URL |
1630
+ | `Image.fromBase64(data, mimeType?)` | Inline base64 (default: `"image/jpeg"`) |
1631
+ | `Image.fromFile(path, mimeType?)` | Local file read synchronously; auto-detects MIME from extension |
1632
+
1633
+ **Serialization helpers (used by adapters internally):**
1634
+
1635
+ ```ts
1636
+ img.toOpenAIContentPart() // { type: "image_url", image_url: { url } }
1637
+ img.toAnthropicContentBlock() // { type: "image", source: { ... } }
1638
+ img.toString() // "[Image: https://...]" — used in text prompts
1639
+ ```
1640
+
1641
+ **Supported MIME types:** `"image/jpeg"`, `"image/png"`, `"image/gif"`, `"image/webp"`
1642
+
1643
+ ---
1644
+
1645
+ ### BootstrapFinetune
1646
+
1647
+ Extends `BootstrapFewShot` to collect execution traces and export them as a
1648
+ JSONL fine-tuning dataset.
1649
+
1650
+ ```ts
1651
+ import { BootstrapFinetune } from "dstsx";
1652
+
1653
+ const optimizer = new BootstrapFinetune({
1654
+ exportPath: "./finetune_data.jsonl", // default
1655
+ format: "openai", // "openai" | "generic"
1656
+ maxBootstrappedDemos: 4,
1657
+ });
1658
+
1659
+ const compiled = await optimizer.compile(program, trainset, metric);
1660
+ // "./finetune_data.jsonl" now contains one JSON object per line:
1661
+ // {"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}
1662
+ ```
1663
+
1664
+ **Options:**
1665
+
1666
+ | Option | Type | Default | Description |
1667
+ |---|---|---|---|
1668
+ | `exportPath` | `string` | `"./finetune_data.jsonl"` | Output file path |
1669
+ | `format` | `"openai" \| "generic"` | `"openai"` | JSONL line format |
1670
+ | `maxBootstrappedDemos` | `number` | `4` | Demos to bootstrap per predictor |
1671
+
1672
+ **Format examples:**
1673
+
1674
+ `"openai"` (suitable for OpenAI fine-tuning):
1675
+ ```jsonl
1676
+ {"messages": [{"role": "user", "content": "question: What is 2+2?"}, {"role": "assistant", "content": "answer: 4"}]}
1677
+ ```
1678
+
1679
+ `"generic"` (plain prompt/completion):
1680
+ ```jsonl
1681
+ {"prompt": "question: What is 2+2?", "completion": "answer: 4"}
1682
+ ```
1683
+
1684
+ ---
1685
+
1686
+ ### GRPO Optimizer
1687
+
1688
+ Group Relative Policy Optimization — mirrors `dspy.GRPO`. Iteratively generates
1689
+ groups of candidate instruction variants, evaluates them relative to each other,
1690
+ and converges toward the best-scoring configuration.
1691
+
1692
+ ```ts
1693
+ import { GRPO } from "dstsx";
1694
+
1695
+ const optimizer = new GRPO({
1696
+ numSteps: 20, // optimization iterations
1697
+ groupSize: 8, // candidates per group
1698
+ temperature: 1.0, // sampling temperature
1699
+ });
1700
+
1701
+ const optimized = await optimizer.compile(program, trainset, metric);
1702
+ ```
1703
+
1704
+ **How it works:** Each step generates `groupSize` instruction alternatives using
1705
+ the configured LM at the specified temperature. All candidates are evaluated on
1706
+ the training set. The relative advantage of each is computed as
1707
+ `(score − mean) / std`. The best-scoring candidate becomes the new baseline for
1708
+ the next step.
1709
+
1710
+ **Options:**
1711
+
1712
+ | Option | Type | Default |
1713
+ |---|---|---|
1714
+ | `numSteps` | `number` | `20` |
1715
+ | `groupSize` | `number` | `8` |
1716
+ | `temperature` | `number` | `1.0` |
1717
+ | `maxLabeledDemos` | `number` | `16` |
1718
+
1719
+ ---
1720
+
1721
+ ### SIMBA Optimizer
1722
+
1723
+ SIMBA (Stochastic Introspective Mini-Batch Ascent) — mirrors `dspy.SIMBA`. A
1724
+ lightweight stochastic optimizer well-suited for small training sets.
1725
+
1726
+ ```ts
1727
+ import { SIMBA } from "dstsx";
1728
+
1729
+ const optimizer = new SIMBA({
1730
+ numIter: 10, // iterations
1731
+ batchSize: 8, // mini-batch size per evaluation
1732
+ maxBootstrappedDemos: 4,
1733
+ });
1734
+
1735
+ const optimized = await optimizer.compile(program, trainset, metric);
1736
+ ```
1737
+
1738
+ **How it works:** Initializes with `BootstrapFewShot`, then for each iteration
1739
+ draws a random mini-batch (Fisher-Yates shuffle), proposes a demo-subset
1740
+ candidate, evaluates it on the batch, and accepts it if it improves on the
1741
+ current best score.
1742
+
1743
+ **Options:**
1744
+
1745
+ | Option | Type | Default |
1746
+ |---|---|---|
1747
+ | `numIter` | `number` | `10` |
1748
+ | `batchSize` | `number` | `8` |
1749
+ | `maxBootstrappedDemos` | `number` | `4` |
1750
+
1751
+ ---
1752
+
1753
+ ### AvatarOptimizer
1754
+
1755
+ Iteratively proposes and evaluates "avatar" role descriptions (persona prefixes)
1756
+ for each `Predict` module, selecting the instruction that scores highest on the
1757
+ training set.
1758
+
1759
+ ```ts
1760
+ import { AvatarOptimizer } from "dstsx";
1761
+
1762
+ const optimizer = new AvatarOptimizer({
1763
+ numAvatars: 4, // candidate personas per predictor
1764
+ maxLabeledDemos: 8,
1765
+ });
1766
+
1767
+ const optimized = await optimizer.compile(program, trainset, metric);
1768
+ ```
1769
+
1770
+ **How it works:** For each `Predict` predictor in the program, asks the
1771
+ configured LM to generate `numAvatars` distinct role/persona descriptions
1772
+ (e.g. "You are an expert doctor…"). Each persona is prepended to the predictor's
1773
+ instructions and scored on the training set. The best persona is kept.
1774
+
1775
+ **Options:**
1776
+
1777
+ | Option | Type | Default |
1778
+ |---|---|---|
1779
+ | `numAvatars` | `number` | `4` |
1780
+ | `maxLabeledDemos` | `number` | `8` |
1781
+
1782
+ ---
1783
+
1784
+ ### Experiment Tracking
1785
+
1786
+ Track optimizer progress (steps, scores, best candidates) to console, JSON
1787
+ files, or custom backends.
1788
+
1789
+ ```ts
1790
+ import { ConsoleTracker, JsonFileTracker, GRPO, SIMBA, AvatarOptimizer } from "dstsx";
1791
+
1792
+ // Log to console
1793
+ const consoleTracker = new ConsoleTracker();
1794
+ // [STEP] step=1 score=0.7500
1795
+ // [BEST] step=3 score=0.8750
1796
+
1797
+ // Log to JSONL file
1798
+ const fileTracker = new JsonFileTracker("./runs/exp1.jsonl");
1799
+ await fileTracker.flush(); // flush buffer to disk
1800
+
1801
+ // Pass to any optimizer
1802
+ const optimizer = new GRPO({ numSteps: 10 });
1803
+ // (trackers are accepted as options on GRPO, SIMBA, AvatarOptimizer)
1804
+ ```
1805
+
1806
+ **Custom tracker — extend `Tracker`:**
1807
+
1808
+ ```ts
1809
+ import { Tracker } from "dstsx";
1810
+ import type { TrackerEvent } from "dstsx";
1811
+
1812
+ class MLflowTracker extends Tracker {
1813
+ log(event: TrackerEvent): void {
1814
+ // Send to MLflow REST API, W&B, etc.
1815
+ console.log("mlflow.log_metric", event.score);
1816
+ }
1817
+ async flush(): Promise<void> {}
1818
+ }
1819
+ ```
1820
+
1821
+ **`TrackerEvent` type:**
1822
+
1823
+ ```ts
1824
+ interface TrackerEvent {
1825
+ type: "step" | "trial" | "best" | "done";
1826
+ step?: number;
1827
+ score?: number;
1828
+ metadata?: Record<string, unknown>;
1829
+ }
1830
+ ```
1831
+
1832
+ **Exported classes:** `Tracker` (abstract), `ConsoleTracker`, `JsonFileTracker`
1833
+
1834
+ ---
1835
+
1836
+ ### Worker-Thread `ProgramOfThought`
1837
+
1838
+ `ProgramOfThought` now supports a `sandbox` option to run generated code in a
1839
+ Node.js Worker thread instead of `new Function()` in the main process, providing
1840
+ stronger isolation.
1841
+
1842
+ ```ts
1843
+ import { ProgramOfThought } from "dstsx";
1844
+
1845
+ // Default: "function" (same process, backwards-compatible)
1846
+ const pot = new ProgramOfThought("question -> answer");
1847
+
1848
+ // Worker thread isolation (Node 18+)
1849
+ const potWorker = new ProgramOfThought(
1850
+ "question -> answer",
1851
+ 3, // maxAttempts
1852
+ 5_000, // timeoutMs
1853
+ "worker" // sandbox mode
1854
+ );
1855
+ const result = await potWorker.forward({ question: "What is 2 ** 10?" });
1856
+ console.log(result.get("answer")); // "1024"
1857
+
1858
+ // Disable timeout/sandbox entirely (not recommended for untrusted input)
1859
+ const potNone = new ProgramOfThought("question -> answer", 3, 5_000, "none");
1860
+ ```
1861
+
1862
+ **Sandbox modes:**
1863
+
1864
+ | Mode | Isolation | True Cancellation | Notes |
1865
+ |---|---|---|---|
1866
+ | `"function"` | None — runs in main process | No | **Default.** Backwards-compatible. |
1867
+ | `"worker"` | Node.js Worker thread | Yes (terminate on timeout) | Requires Node 18+. |
1868
+ | `"none"` | None — no timeout applied | N/A | Fastest; do not use with untrusted code. |
1869
+
1870
+ ---
1871
+
1872
+ ## End-to-End Examples
1873
+
1874
+ ### 1. Simple Q&A
1875
+
1876
+ ```ts
1877
+ import { settings, OpenAI, Predict } from "dstsx";
1878
+
1879
+ settings.configure({ lm: new OpenAI({ model: "gpt-4o-mini" }) });
1880
+
1881
+ const qa = new Predict("question -> answer");
1882
+
1883
+ const result = await qa.forward({ question: "What is the speed of light?" });
1884
+ console.log(result.get("answer"));
1885
+ ```
1886
+
1887
+ ---
1888
+
1889
+ ### 2. Retrieval-Augmented Generation (RAG)
1890
+
1891
+ ```ts
1892
+ import {
1893
+ Module, Retrieve, ChainOfThought, ColBERTv2,
1894
+ settings, OpenAI, type Prediction,
1895
+ } from "dstsx";
1896
+
1897
+ settings.configure({
1898
+ lm: new OpenAI({ model: "gpt-4o" }),
1899
+ rm: new ColBERTv2("http://localhost:8893"),
1900
+ });
1901
+
1902
+ class RAG extends Module {
1903
+ retrieve = new Retrieve(3);
1904
+ generate = new ChainOfThought("context, question -> answer");
1905
+
1906
+ async forward(inputs: { question: string }): Promise<Prediction> {
1907
+ const { passages } = (await this.retrieve.forward(inputs.question)).toDict() as { passages: string[] };
1908
+ return this.generate.forward({
1909
+ context: passages.join("\n"),
1910
+ question: inputs.question,
1911
+ });
1912
+ }
1913
+ }
1914
+
1915
+ const rag = new RAG();
1916
+ const result = await rag.forward({ question: "What is the capital of Germany?" });
1917
+ console.log(result.get("answer")); // "Berlin"
1918
+ ```
1919
+
1920
+ ---
1921
+
1922
+ ### 3. Optimizing with BootstrapFewShot
1923
+
1924
+ ```ts
1925
+ import {
1926
+ settings, MockLM, Predict, Module, BootstrapFewShot,
1927
+ Example, evaluate, exactMatch, type Prediction,
1928
+ } from "dstsx";
1929
+
1930
+ settings.configure({ lm: new MockLM({}, "answer: 42") });
1931
+
1932
+ class QA extends Module {
1933
+ predict = new Predict("question -> answer");
1934
+ async forward(inputs: { question: string }): Promise<Prediction> {
1935
+ return this.predict.forward(inputs);
1936
+ }
1937
+ }
1938
+
1939
+ const trainset = [
1940
+ new Example({ question: "What is 6 × 7?", answer: "42" }),
1941
+ new Example({ question: "What is 8 × 8?", answer: "64" }),
1942
+ ];
1943
+
1944
+ const optimizer = new BootstrapFewShot({ maxBootstrappedDemos: 2 });
1945
+ const optimized = await optimizer.compile(new QA(), trainset, exactMatch("answer"));
1946
+
1947
+ // Persist
1948
+ import { writeFileSync } from "fs";
1949
+ writeFileSync("qa_optimized.json", JSON.stringify(optimized.dump(), null, 2));
1950
+ ```
1951
+
1952
+ ---
1953
+
1954
+ ### 4. ReAct Agent
1955
+
1956
+ ```ts
1957
+ import { settings, OpenAI, ReAct, type Tool } from "dstsx";
1958
+
1959
+ settings.configure({ lm: new OpenAI({ model: "gpt-4o" }) });
1960
+
1961
+ const tools: Tool[] = [
1962
+ {
1963
+ name: "calculator",
1964
+ description: "Evaluates a mathematical expression and returns the numeric result",
1965
+ fn: async (expr) => String(Function(`"use strict"; return (${expr})`)()),
1966
+ },
1967
+ {
1968
+ name: "lookup",
1969
+ description: "Looks up a fact in the knowledge base",
1970
+ fn: async (query) => `Fact about ${query}: (result from KB)`,
1971
+ },
1972
+ ];
1973
+
1974
+ const agent = new ReAct("question -> answer", tools, /* maxIter= */ 6);
1975
+ const result = await agent.forward({ question: "What is (123 * 456) + 789?" });
1976
+ console.log(result.get("answer"));
1977
+ console.log(result.get("trajectory"));
1978
+ ```
1979
+
1980
+ ---
1981
+
1982
+ ### 5. Assertions with Retry
1983
+
1984
+ ```ts
1985
+ import {
1986
+ settings, MockLM, Module, Predict, Retry, Assert, type Prediction,
1987
+ } from "dstsx";
1988
+
1989
+ settings.configure({ lm: new MockLM({}, "answer: Paris") });
1990
+
1991
+ class CapitalQA extends Module {
1992
+ predict = new Predict("question, feedback? -> answer");
1993
+
1994
+ async forward(inputs: { question: string }): Promise<Prediction> {
1995
+ const result = await this.predict.forward(inputs);
1996
+ Assert(
1997
+ String(result.get("answer")).trim().length > 0,
1998
+ "Answer must not be empty"
1999
+ );
2000
+ return result;
2001
+ }
2002
+ }
2003
+
2004
+ const retrying = new Retry(new CapitalQA(), 3);
2005
+ const result = await retrying.forward({ question: "What is the capital of France?" });
2006
+ console.log(result.get("answer")); // "Paris"
2007
+ ```
2008
+
2009
+ ---
2010
+
2011
+ ### 6. Per-Request LM Override (server environments)
2012
+
2013
+ ```ts
2014
+ import express from "express";
2015
+ import { settings, OpenAI, Predict } from "dstsx";
2016
+
2017
+ const app = express();
2018
+ const qa = new Predict("question -> answer");
2019
+ const gpt4 = new OpenAI({ model: "gpt-4o" });
2020
+ const gptMini = new OpenAI({ model: "gpt-4o-mini" });
2021
+
2022
+ settings.configure({ lm: gpt4 }); // global default
2023
+
2024
+ app.get("/fast", async (req, res) => {
2025
+ // Override LM for this request only — concurrent requests never interfere
2026
+ const result = await settings.context(
2027
+ { lm: gptMini },
2028
+ () => qa.forward({ question: req.query["q"] as string }),
2029
+ );
2030
+ res.json(result.toJSON());
2031
+ });
2032
+
2033
+ app.listen(3000);
2034
+ ```
2035
+
2036
+ ---
2037
+
2038
+ ## V2 Roadmap
2039
+
2040
+ ## Contributing & Releases
2041
+
2042
+ To publish new versions of `dstsx`, follow the workflow below:
2043
+
2044
+ 1. Update the package version (`npm version patch`, `minor`, or `major`).
2045
+ 2. Push commits and tags to GitHub (`git push && git push --tags`).
2046
+ 3. Create a GitHub release for the new tag (manual or via `gh` CLI). The
2047
+ existing GitHub Action (`.github/workflows/publish.yml`) triggers on a
2048
+ **published** release and will run tests + build and then publish the
2049
+ package to npm using the `NPM_TOKEN` secret.
2050
+ 4. For local manual publishes you can run `npm publish`; `prepublishOnly` runs
2051
+ the test suite and builds automatically.
2052
+
2053
+ Contributions are welcome! Please open issues or pull requests against the
2054
+ GitHub repository.
2055
+
2056
+
2057
+ ## V2 Roadmap
2058
+
2059
+ The following features are ✅ **implemented in v2**. See [V2_ROADMAP.md](./V2_ROADMAP.md) for details.
2060
+
2061
+ | Feature | DSPy Equivalent | Status |
2062
+ |---|---|---|
2063
+ | **`TypedPredictor`** — JSON-schema + optional Zod validation | `dspy.TypedPredictor`, `dspy.TypedChainOfThought` | ✅ v2 |
2064
+ | **`Parallel`** module — fan-out / fan-in concurrency | `dspy.Parallel` | ✅ v2 |
2065
+ | **`Refine`** module — self-critique loop | — | ✅ v2 |
2066
+ | **`majority()`** helper — vote across Predictions | `dspy.majority` | ✅ v2 |
2067
+ | **`BootstrapFewShotWithOptuna`** — TPE Bayesian search | `dspy.BootstrapFewShotWithOptuna` | ✅ v2 |
2068
+ | **Disk-persistent LM cache** — file-based LRU | `dspy.cache` | ✅ v2 |
2069
+ | **MCP Integration** — `MCPToolAdapter` + `DSTsxMCPServer` | — | ✅ v2 |
2070
+ | **LM Streaming** — `lm.stream()`, `predict.stream()` | `dspy.streamify` | ✅ v2 |
2071
+ | **`NativeReAct`** — OpenAI functions / Anthropic tool use | `dspy.Tool` (v2) | ✅ v2 |
2072
+ | **`Image`** — multi-modal image inputs | `dspy.Image` | ✅ v2 |
2073
+ | **`BootstrapFinetune`** — JSONL fine-tuning export | `dspy.BootstrapFinetune` | ✅ v2 |
2074
+ | **`GRPO`** optimizer — group relative policy optimization | `dspy.GRPO` | ✅ v2 |
2075
+ | **`SIMBA`** optimizer — stochastic mini-batch ascent | `dspy.SIMBA` | ✅ v2 |
2076
+ | **`AvatarOptimizer`** — role/persona prompt optimization | `dspy.AvatarOptimizer` | ✅ v2 |
2077
+ | **Experiment Tracking** — `ConsoleTracker`, `JsonFileTracker` | `dspy.MLflow` | ✅ v2 |
2078
+ | **Worker-thread `ProgramOfThought`** — `sandbox: "worker"` | — | ✅ v2 |
2079
+ | **Typedoc config** — `typedoc.json` + `npm run docs` | — | ✅ v2 |
2080
+ | **GitHub Actions** — CI + npm publish workflows | — | ✅ v2 |
2081
+ | Cross-language trace sharing | — | 🔭 Stretch |
2082
+ | Browser-native bundle (`dstsx/browser`) | — | 🔭 Stretch |
2083
+ | HTTP module serving (REST endpoint) | — | 🔭 Stretch |
2084
+
2085
+ ---
2086
+
2087
+ ## License
2088
+
2089
+ MIT