@every-env/compound-plugin 0.3.0 → 0.5.1

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.
Files changed (50) hide show
  1. package/{plugins/compound-engineering → .claude}/commands/release-docs.md +0 -1
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/.github/workflows/ci.yml +1 -1
  4. package/.github/workflows/deploy-docs.yml +3 -3
  5. package/.github/workflows/publish.yml +37 -0
  6. package/README.md +12 -3
  7. package/docs/index.html +13 -13
  8. package/docs/pages/changelog.html +39 -0
  9. package/docs/plans/2026-02-08-feat-convert-local-md-settings-for-opencode-codex-plan.md +143 -0
  10. package/docs/plans/2026-02-08-feat-simplify-plugin-settings-plan.md +195 -0
  11. package/docs/plans/2026-02-09-refactor-dspy-ruby-skill-update-plan.md +104 -0
  12. package/docs/plans/2026-02-12-feat-add-cursor-cli-target-provider-plan.md +306 -0
  13. package/docs/specs/cursor.md +85 -0
  14. package/package.json +1 -1
  15. package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
  16. package/plugins/compound-engineering/CHANGELOG.md +38 -0
  17. package/plugins/compound-engineering/README.md +5 -3
  18. package/plugins/compound-engineering/commands/workflows/brainstorm.md +6 -1
  19. package/plugins/compound-engineering/commands/workflows/compound.md +1 -0
  20. package/plugins/compound-engineering/commands/workflows/review.md +23 -21
  21. package/plugins/compound-engineering/commands/workflows/work.md +29 -15
  22. package/plugins/compound-engineering/skills/dspy-ruby/SKILL.md +539 -396
  23. package/plugins/compound-engineering/skills/dspy-ruby/assets/config-template.rb +159 -331
  24. package/plugins/compound-engineering/skills/dspy-ruby/assets/module-template.rb +210 -236
  25. package/plugins/compound-engineering/skills/dspy-ruby/assets/signature-template.rb +173 -95
  26. package/plugins/compound-engineering/skills/dspy-ruby/references/core-concepts.md +552 -143
  27. package/plugins/compound-engineering/skills/dspy-ruby/references/observability.md +366 -0
  28. package/plugins/compound-engineering/skills/dspy-ruby/references/optimization.md +440 -460
  29. package/plugins/compound-engineering/skills/dspy-ruby/references/providers.md +305 -225
  30. package/plugins/compound-engineering/skills/dspy-ruby/references/toolsets.md +502 -0
  31. package/plugins/compound-engineering/skills/setup/SKILL.md +168 -0
  32. package/src/commands/convert.ts +10 -5
  33. package/src/commands/install.ts +18 -10
  34. package/src/converters/claude-to-codex.ts +7 -2
  35. package/src/converters/claude-to-cursor.ts +166 -0
  36. package/src/converters/claude-to-droid.ts +174 -0
  37. package/src/converters/claude-to-opencode.ts +8 -2
  38. package/src/targets/cursor.ts +48 -0
  39. package/src/targets/droid.ts +50 -0
  40. package/src/targets/index.ts +18 -0
  41. package/src/types/cursor.ts +29 -0
  42. package/src/types/droid.ts +20 -0
  43. package/tests/cli.test.ts +62 -0
  44. package/tests/codex-converter.test.ts +62 -0
  45. package/tests/converter.test.ts +61 -0
  46. package/tests/cursor-converter.test.ts +347 -0
  47. package/tests/cursor-writer.test.ts +137 -0
  48. package/tests/droid-converter.test.ts +277 -0
  49. package/tests/droid-writer.test.ts +100 -0
  50. package/plugins/compound-engineering/commands/technical_review.md +0 -8
@@ -1,265 +1,674 @@
1
1
  # DSPy.rb Core Concepts
2
2
 
3
- ## Philosophy
3
+ ## Signatures
4
4
 
5
- DSPy.rb enables developers to **program LLMs, not prompt them**. Instead of manually crafting prompts, define application requirements through code using type-safe, composable modules.
5
+ Signatures define the interface between application code and language models. They specify inputs, outputs, and a task description using Sorbet types for compile-time and runtime type safety.
6
6
 
7
- ## Signatures
7
+ ### Structure
8
8
 
9
- Signatures define type-safe input/output contracts for LLM operations. They specify what data goes in and what data comes out, with runtime type checking.
9
+ ```ruby
10
+ class ClassifyEmail < DSPy::Signature
11
+ description "Classify customer support emails by urgency and category"
10
12
 
11
- ### Basic Signature Structure
13
+ input do
14
+ const :subject, String
15
+ const :body, String
16
+ end
17
+
18
+ output do
19
+ const :category, String
20
+ const :urgency, String
21
+ end
22
+ end
23
+ ```
24
+
25
+ ### Supported Types
26
+
27
+ | Type | JSON Schema | Notes |
28
+ |------|-------------|-------|
29
+ | `String` | `string` | Required string |
30
+ | `Integer` | `integer` | Whole numbers |
31
+ | `Float` | `number` | Decimal numbers |
32
+ | `T::Boolean` | `boolean` | true/false |
33
+ | `T::Array[X]` | `array` | Typed arrays |
34
+ | `T::Hash[K, V]` | `object` | Typed key-value maps |
35
+ | `T.nilable(X)` | nullable | Optional fields |
36
+ | `Date` | `string` (ISO 8601) | Auto-converted |
37
+ | `DateTime` | `string` (ISO 8601) | Preserves timezone |
38
+ | `Time` | `string` (ISO 8601) | Converted to UTC |
39
+
40
+ ### Date and Time Types
41
+
42
+ Date, DateTime, and Time fields serialize to ISO 8601 strings and auto-convert back to Ruby objects on output.
12
43
 
13
44
  ```ruby
14
- class TaskSignature < DSPy::Signature
15
- description "Brief description of what this signature does"
45
+ class EventScheduler < DSPy::Signature
46
+ description "Schedule events based on requirements"
16
47
 
17
48
  input do
18
- const :field_name, String, desc: "Description of this input field"
19
- const :another_field, Integer, desc: "Another input field"
49
+ const :start_date, Date # ISO 8601: YYYY-MM-DD
50
+ const :preferred_time, DateTime # ISO 8601 with timezone
51
+ const :deadline, Time # Converted to UTC
52
+ const :end_date, T.nilable(Date) # Optional date
20
53
  end
21
54
 
22
55
  output do
23
- const :result_field, String, desc: "Description of the output"
24
- const :confidence, Float, desc: "Confidence score (0.0-1.0)"
56
+ const :scheduled_date, Date # String from LLM, auto-converted to Date
57
+ const :event_datetime, DateTime # Preserves timezone info
58
+ const :created_at, Time # Converted to UTC
25
59
  end
26
60
  end
61
+
62
+ predictor = DSPy::Predict.new(EventScheduler)
63
+ result = predictor.call(
64
+ start_date: "2024-01-15",
65
+ preferred_time: "2024-01-15T10:30:45Z",
66
+ deadline: Time.now,
67
+ end_date: nil
68
+ )
69
+
70
+ result.scheduled_date.class # => Date
71
+ result.event_datetime.class # => DateTime
27
72
  ```
28
73
 
29
- ### Type Safety
74
+ Timezone conventions follow ActiveRecord: Time objects convert to UTC, DateTime objects preserve timezone, Date objects are timezone-agnostic.
75
+
76
+ ### Enums with T::Enum
77
+
78
+ Define constrained output values using `T::Enum` classes. Do not use inline `T.enum([...])` syntax.
79
+
80
+ ```ruby
81
+ class SentimentAnalysis < DSPy::Signature
82
+ description "Analyze sentiment of text"
83
+
84
+ class Sentiment < T::Enum
85
+ enums do
86
+ Positive = new('positive')
87
+ Negative = new('negative')
88
+ Neutral = new('neutral')
89
+ end
90
+ end
91
+
92
+ input do
93
+ const :text, String
94
+ end
95
+
96
+ output do
97
+ const :sentiment, Sentiment
98
+ const :confidence, Float
99
+ end
100
+ end
30
101
 
31
- Signatures support Sorbet types including:
32
- - `String` - Text data
33
- - `Integer`, `Float` - Numeric data
34
- - `T::Boolean` - Boolean values
35
- - `T::Array[Type]` - Arrays of specific types
36
- - Custom enums and classes
102
+ predictor = DSPy::Predict.new(SentimentAnalysis)
103
+ result = predictor.call(text: "This product is amazing!")
104
+
105
+ result.sentiment # => #<Sentiment::Positive>
106
+ result.sentiment.serialize # => "positive"
107
+ result.confidence # => 0.92
108
+ ```
109
+
110
+ Enum matching is case-insensitive. The LLM returning `"POSITIVE"` matches `new('positive')`.
111
+
112
+ ### Default Values
113
+
114
+ Default values work on both inputs and outputs. Input defaults reduce caller boilerplate. Output defaults provide fallbacks when the LLM omits optional fields.
115
+
116
+ ```ruby
117
+ class SmartSearch < DSPy::Signature
118
+ description "Search with intelligent defaults"
119
+
120
+ input do
121
+ const :query, String
122
+ const :max_results, Integer, default: 10
123
+ const :language, String, default: "English"
124
+ end
125
+
126
+ output do
127
+ const :results, T::Array[String]
128
+ const :total_found, Integer
129
+ const :cached, T::Boolean, default: false
130
+ end
131
+ end
132
+
133
+ search = DSPy::Predict.new(SmartSearch)
134
+ result = search.call(query: "Ruby programming")
135
+ # max_results defaults to 10, language defaults to "English"
136
+ # If LLM omits `cached`, it defaults to false
137
+ ```
37
138
 
38
139
  ### Field Descriptions
39
140
 
40
- Always provide clear field descriptions using the `desc:` parameter. These descriptions:
41
- - Guide the LLM on expected input/output format
42
- - Serve as documentation for developers
43
- - Improve prediction accuracy
141
+ Add `description:` to any field to guide the LLM on expected content. These descriptions appear in the generated JSON schema sent to the model.
142
+
143
+ ```ruby
144
+ class ASTNode < T::Struct
145
+ const :node_type, String, description: "The type of AST node (heading, paragraph, code_block)"
146
+ const :text, String, default: "", description: "Text content of the node"
147
+ const :level, Integer, default: 0, description: "Heading level 1-6, only for heading nodes"
148
+ const :children, T::Array[ASTNode], default: []
149
+ end
150
+
151
+ ASTNode.field_descriptions[:node_type] # => "The type of AST node ..."
152
+ ASTNode.field_descriptions[:children] # => nil (no description set)
153
+ ```
154
+
155
+ Field descriptions also work inside signature `input` and `output` blocks:
156
+
157
+ ```ruby
158
+ class ExtractEntities < DSPy::Signature
159
+ description "Extract named entities from text"
160
+
161
+ input do
162
+ const :text, String, description: "Raw text to analyze"
163
+ const :language, String, default: "en", description: "ISO 639-1 language code"
164
+ end
165
+
166
+ output do
167
+ const :entities, T::Array[String], description: "List of extracted entity names"
168
+ const :count, Integer, description: "Total number of unique entities found"
169
+ end
170
+ end
171
+ ```
172
+
173
+ ### Schema Formats
174
+
175
+ DSPy.rb supports three schema formats for communicating type structure to LLMs.
176
+
177
+ #### JSON Schema (default)
178
+
179
+ Verbose but universally supported. Access via `YourSignature.output_json_schema`.
180
+
181
+ #### BAML Schema
182
+
183
+ Compact format that reduces schema tokens by 80-85%. Requires the `sorbet-baml` gem.
184
+
185
+ ```ruby
186
+ DSPy.configure do |c|
187
+ c.lm = DSPy::LM.new('openai/gpt-4o-mini',
188
+ api_key: ENV['OPENAI_API_KEY'],
189
+ schema_format: :baml
190
+ )
191
+ end
192
+ ```
193
+
194
+ BAML applies only in Enhanced Prompting mode (`structured_outputs: false`). When `structured_outputs: true`, the provider receives JSON Schema directly.
195
+
196
+ #### TOON Schema + Data Format
197
+
198
+ Table-oriented text format that shrinks both schema definitions and prompt values.
199
+
200
+ ```ruby
201
+ DSPy.configure do |c|
202
+ c.lm = DSPy::LM.new('openai/gpt-4o-mini',
203
+ api_key: ENV['OPENAI_API_KEY'],
204
+ schema_format: :toon,
205
+ data_format: :toon
206
+ )
207
+ end
208
+ ```
209
+
210
+ `schema_format: :toon` replaces the schema block in the system prompt. `data_format: :toon` renders input values and output templates inside `toon` fences. Only works with Enhanced Prompting mode. The `sorbet-toon` gem is included automatically as a dependency.
211
+
212
+ ### Recursive Types
213
+
214
+ Structs that reference themselves produce `$defs` entries in the generated JSON schema, using `$ref` pointers to avoid infinite recursion.
215
+
216
+ ```ruby
217
+ class ASTNode < T::Struct
218
+ const :node_type, String
219
+ const :text, String, default: ""
220
+ const :children, T::Array[ASTNode], default: []
221
+ end
222
+ ```
223
+
224
+ The schema generator detects the self-reference in `T::Array[ASTNode]` and emits:
225
+
226
+ ```json
227
+ {
228
+ "$defs": {
229
+ "ASTNode": { "type": "object", "properties": { ... } }
230
+ },
231
+ "properties": {
232
+ "children": {
233
+ "type": "array",
234
+ "items": { "$ref": "#/$defs/ASTNode" }
235
+ }
236
+ }
237
+ }
238
+ ```
239
+
240
+ Access the schema with accumulated definitions via `YourSignature.output_json_schema_with_defs`.
241
+
242
+ ### Union Types with T.any()
243
+
244
+ Specify fields that accept multiple types:
245
+
246
+ ```ruby
247
+ output do
248
+ const :result, T.any(Float, String)
249
+ end
250
+ ```
251
+
252
+ For struct unions, DSPy.rb automatically adds a `_type` discriminator field to each struct's JSON schema. The LLM returns `_type` in its response, and DSPy converts the hash to the correct struct instance.
253
+
254
+ ```ruby
255
+ class CreateTask < T::Struct
256
+ const :title, String
257
+ const :priority, String
258
+ end
259
+
260
+ class DeleteTask < T::Struct
261
+ const :task_id, String
262
+ const :reason, T.nilable(String)
263
+ end
264
+
265
+ class TaskRouter < DSPy::Signature
266
+ description "Route user request to the appropriate task action"
267
+
268
+ input do
269
+ const :request, String
270
+ end
271
+
272
+ output do
273
+ const :action, T.any(CreateTask, DeleteTask)
274
+ end
275
+ end
276
+
277
+ result = DSPy::Predict.new(TaskRouter).call(request: "Create a task for Q4 review")
278
+ result.action.class # => CreateTask
279
+ result.action.title # => "Q4 Review"
280
+ ```
281
+
282
+ Pattern matching works on the result:
283
+
284
+ ```ruby
285
+ case result.action
286
+ when CreateTask then puts "Creating: #{result.action.title}"
287
+ when DeleteTask then puts "Deleting: #{result.action.task_id}"
288
+ end
289
+ ```
290
+
291
+ Union types also work inside arrays for heterogeneous collections:
292
+
293
+ ```ruby
294
+ output do
295
+ const :events, T::Array[T.any(LoginEvent, PurchaseEvent)]
296
+ end
297
+ ```
298
+
299
+ Limit unions to 2-4 types for reliable LLM comprehension. Use clear struct names since they become the `_type` discriminator values.
300
+
301
+ ---
44
302
 
45
303
  ## Modules
46
304
 
47
- Modules are composable building blocks that use signatures to perform LLM operations. They can be chained together to create complex workflows.
305
+ Modules are composable building blocks that wrap predictors. Define a `forward` method; invoke the module with `.call()`.
48
306
 
49
- ### Basic Module Structure
307
+ ### Basic Structure
50
308
 
51
309
  ```ruby
52
- class MyModule < DSPy::Module
310
+ class SentimentAnalyzer < DSPy::Module
53
311
  def initialize
54
312
  super
55
- @predictor = DSPy::Predict.new(MySignature)
313
+ @predictor = DSPy::Predict.new(SentimentSignature)
56
314
  end
57
315
 
58
- def forward(input_hash)
59
- @predictor.forward(input_hash)
316
+ def forward(text:)
317
+ @predictor.call(text: text)
60
318
  end
61
319
  end
320
+
321
+ analyzer = SentimentAnalyzer.new
322
+ result = analyzer.call(text: "I love this product!")
323
+
324
+ result.sentiment # => "positive"
325
+ result.confidence # => 0.9
62
326
  ```
63
327
 
328
+ **API rules:**
329
+ - Invoke modules and predictors with `.call()`, not `.forward()`.
330
+ - Access result fields with `result.field`, not `result[:field]`.
331
+
64
332
  ### Module Composition
65
333
 
66
- Modules can call other modules to create pipelines:
334
+ Combine multiple modules through explicit method calls in `forward`:
67
335
 
68
336
  ```ruby
69
- class ComplexWorkflow < DSPy::Module
337
+ class DocumentProcessor < DSPy::Module
338
+ def initialize
339
+ super
340
+ @classifier = DocumentClassifier.new
341
+ @summarizer = DocumentSummarizer.new
342
+ end
343
+
344
+ def forward(document:)
345
+ classification = @classifier.call(content: document)
346
+ summary = @summarizer.call(content: document)
347
+
348
+ {
349
+ document_type: classification.document_type,
350
+ summary: summary.summary
351
+ }
352
+ end
353
+ end
354
+ ```
355
+
356
+ ### Lifecycle Callbacks
357
+
358
+ Modules support `before`, `after`, and `around` callbacks on `forward`. Declare them as class-level macros referencing private methods.
359
+
360
+ #### Execution order
361
+
362
+ 1. `before` callbacks (in registration order)
363
+ 2. `around` callbacks (before `yield`)
364
+ 3. `forward` method
365
+ 4. `around` callbacks (after `yield`)
366
+ 5. `after` callbacks (in registration order)
367
+
368
+ ```ruby
369
+ class InstrumentedModule < DSPy::Module
370
+ before :setup_metrics
371
+ after :log_metrics
372
+ around :manage_context
373
+
374
+ def initialize
375
+ super
376
+ @predictor = DSPy::Predict.new(MySignature)
377
+ @metrics = {}
378
+ end
379
+
380
+ def forward(question:)
381
+ @predictor.call(question: question)
382
+ end
383
+
384
+ private
385
+
386
+ def setup_metrics
387
+ @metrics[:start_time] = Time.now
388
+ end
389
+
390
+ def manage_context
391
+ load_context
392
+ result = yield
393
+ save_context
394
+ result
395
+ end
396
+
397
+ def log_metrics
398
+ @metrics[:duration] = Time.now - @metrics[:start_time]
399
+ end
400
+ end
401
+ ```
402
+
403
+ Multiple callbacks of the same type execute in registration order. Callbacks inherit from parent classes; parent callbacks run first.
404
+
405
+ #### Around callbacks
406
+
407
+ Around callbacks must call `yield` to execute the wrapped method and return the result:
408
+
409
+ ```ruby
410
+ def with_retry
411
+ retries = 0
412
+ begin
413
+ yield
414
+ rescue StandardError => e
415
+ retries += 1
416
+ retry if retries < 3
417
+ raise e
418
+ end
419
+ end
420
+ ```
421
+
422
+ ### Instruction Update Contract
423
+
424
+ Teleprompters (GEPA, MIPROv2) require modules to expose immutable update hooks. Include `DSPy::Mixins::InstructionUpdatable` and implement `with_instruction` and `with_examples`, each returning a new instance:
425
+
426
+ ```ruby
427
+ class SentimentPredictor < DSPy::Module
428
+ include DSPy::Mixins::InstructionUpdatable
429
+
70
430
  def initialize
71
431
  super
72
- @step1 = FirstModule.new
73
- @step2 = SecondModule.new
432
+ @predictor = DSPy::Predict.new(SentimentSignature)
74
433
  end
75
434
 
76
- def forward(input)
77
- result1 = @step1.forward(input)
78
- result2 = @step2.forward(result1)
79
- result2
435
+ def with_instruction(instruction)
436
+ clone = self.class.new
437
+ clone.instance_variable_set(:@predictor, @predictor.with_instruction(instruction))
438
+ clone
439
+ end
440
+
441
+ def with_examples(examples)
442
+ clone = self.class.new
443
+ clone.instance_variable_set(:@predictor, @predictor.with_examples(examples))
444
+ clone
80
445
  end
81
446
  end
82
447
  ```
83
448
 
449
+ If a module omits these hooks, teleprompters raise `DSPy::InstructionUpdateError` instead of silently mutating state.
450
+
451
+ ---
452
+
84
453
  ## Predictors
85
454
 
86
- Predictors are the core execution engines that take signatures and perform LLM inference. DSPy.rb provides several predictor types.
455
+ Predictors are execution engines that take a signature and produce structured results from a language model. DSPy.rb provides four predictor types.
87
456
 
88
457
  ### Predict
89
458
 
90
- Basic LLM inference with type-safe inputs and outputs.
459
+ Direct LLM call with typed input/output. Fastest option, lowest token usage.
91
460
 
92
461
  ```ruby
93
- predictor = DSPy::Predict.new(TaskSignature)
94
- result = predictor.forward(field_name: "value", another_field: 42)
95
- # Returns: { result_field: "...", confidence: 0.85 }
462
+ classifier = DSPy::Predict.new(ClassifyText)
463
+ result = classifier.call(text: "Technical document about APIs")
464
+
465
+ result.sentiment # => #<Sentiment::Positive>
466
+ result.topics # => ["APIs", "technical"]
467
+ result.confidence # => 0.92
96
468
  ```
97
469
 
98
470
  ### ChainOfThought
99
471
 
100
- Automatically adds a reasoning field to the output, improving accuracy for complex tasks.
472
+ Adds a `reasoning` field to the output automatically. The model generates step-by-step reasoning before the final answer. Do not define a `:reasoning` field in the signature output when using ChainOfThought.
101
473
 
102
474
  ```ruby
103
- class EmailClassificationSignature < DSPy::Signature
104
- description "Classify customer support emails"
475
+ class SolveMathProblem < DSPy::Signature
476
+ description "Solve mathematical word problems step by step"
105
477
 
106
478
  input do
107
- const :email_subject, String
108
- const :email_body, String
479
+ const :problem, String
109
480
  end
110
481
 
111
482
  output do
112
- const :category, String # "Technical", "Billing", or "General"
113
- const :priority, String # "High", "Medium", or "Low"
483
+ const :answer, String
484
+ # :reasoning is added automatically by ChainOfThought
114
485
  end
115
486
  end
116
487
 
117
- predictor = DSPy::ChainOfThought.new(EmailClassificationSignature)
118
- result = predictor.forward(
119
- email_subject: "Can't log in to my account",
120
- email_body: "I've been trying to access my account for hours..."
121
- )
122
- # Returns: {
123
- # reasoning: "This appears to be a technical issue...",
124
- # category: "Technical",
125
- # priority: "High"
126
- # }
488
+ solver = DSPy::ChainOfThought.new(SolveMathProblem)
489
+ result = solver.call(problem: "Sarah has 15 apples. She gives 7 away and buys 12 more.")
490
+
491
+ result.reasoning # => "Step by step: 15 - 7 = 8, then 8 + 12 = 20"
492
+ result.answer # => "20 apples"
127
493
  ```
128
494
 
495
+ Use ChainOfThought for complex analysis, multi-step reasoning, or when explainability matters.
496
+
129
497
  ### ReAct
130
498
 
131
- Tool-using agents with iterative reasoning. Enables autonomous problem-solving by allowing the LLM to use external tools.
499
+ Reasoning + Action agent that uses tools in an iterative loop. Define tools by subclassing `DSPy::Tools::Base`. Group related tools with `DSPy::Tools::Toolset`.
132
500
 
133
501
  ```ruby
134
- class SearchTool < DSPy::Tool
135
- def call(query:)
136
- # Perform search and return results
137
- { results: search_database(query) }
502
+ class WeatherTool < DSPy::Tools::Base
503
+ extend T::Sig
504
+
505
+ tool_name "weather"
506
+ tool_description "Get weather information for a location"
507
+
508
+ sig { params(location: String).returns(String) }
509
+ def call(location:)
510
+ { location: location, temperature: 72, condition: "sunny" }.to_json
138
511
  end
139
512
  end
140
513
 
141
- predictor = DSPy::ReAct.new(
142
- TaskSignature,
143
- tools: [SearchTool.new],
514
+ class TravelSignature < DSPy::Signature
515
+ description "Help users plan travel"
516
+
517
+ input do
518
+ const :destination, String
519
+ end
520
+
521
+ output do
522
+ const :recommendations, String
523
+ end
524
+ end
525
+
526
+ agent = DSPy::ReAct.new(
527
+ TravelSignature,
528
+ tools: [WeatherTool.new],
144
529
  max_iterations: 5
145
530
  )
531
+
532
+ result = agent.call(destination: "Tokyo, Japan")
533
+ result.recommendations # => "Visit Senso-ji Temple early morning..."
534
+ result.history # => Array of reasoning steps, actions, observations
535
+ result.iterations # => 3
536
+ result.tools_used # => ["weather"]
537
+ ```
538
+
539
+ Use toolsets to expose multiple tool methods from a single class:
540
+
541
+ ```ruby
542
+ text_tools = DSPy::Tools::TextProcessingToolset.to_tools
543
+ agent = DSPy::ReAct.new(MySignature, tools: text_tools)
146
544
  ```
147
545
 
148
546
  ### CodeAct
149
547
 
150
- Dynamic code generation for solving problems programmatically. Requires the optional `dspy-code_act` gem.
548
+ Think-Code-Observe agent that synthesizes and executes Ruby code. Ships as a separate gem.
549
+
550
+ ```ruby
551
+ # Gemfile
552
+ gem 'dspy-code_act', '~> 0.29'
553
+ ```
151
554
 
152
555
  ```ruby
153
- predictor = DSPy::CodeAct.new(TaskSignature)
154
- result = predictor.forward(task: "Calculate the factorial of 5")
155
- # The LLM generates and executes Ruby code to solve the task
556
+ programmer = DSPy::CodeAct.new(ProgrammingSignature, max_iterations: 10)
557
+ result = programmer.call(task: "Calculate the factorial of 20")
156
558
  ```
157
559
 
158
- ## Multimodal Support
560
+ ### Predictor Comparison
159
561
 
160
- DSPy.rb supports vision capabilities across compatible models using the unified `DSPy::Image` interface.
562
+ | Predictor | Speed | Token Usage | Best For |
563
+ |-----------|-------|-------------|----------|
564
+ | Predict | Fastest | Low | Classification, extraction |
565
+ | ChainOfThought | Moderate | Medium-High | Complex reasoning, analysis |
566
+ | ReAct | Slower | High | Multi-step tasks with tools |
567
+ | CodeAct | Slowest | Very High | Dynamic programming, calculations |
568
+
569
+ ### Concurrent Predictions
570
+
571
+ Process multiple independent predictions simultaneously using `Async::Barrier`:
161
572
 
162
573
  ```ruby
163
- class VisionSignature < DSPy::Signature
164
- description "Describe what's in an image"
574
+ require 'async'
575
+ require 'async/barrier'
165
576
 
166
- input do
167
- const :image, DSPy::Image
168
- const :question, String
169
- end
577
+ analyzer = DSPy::Predict.new(ContentAnalyzer)
578
+ documents = ["Text one", "Text two", "Text three"]
170
579
 
171
- output do
172
- const :description, String
580
+ Async do
581
+ barrier = Async::Barrier.new
582
+
583
+ tasks = documents.map do |doc|
584
+ barrier.async { analyzer.call(content: doc) }
173
585
  end
586
+
587
+ barrier.wait
588
+ predictions = tasks.map(&:wait)
589
+
590
+ predictions.each { |p| puts p.sentiment }
174
591
  end
592
+ ```
175
593
 
176
- predictor = DSPy::Predict.new(VisionSignature)
177
- result = predictor.forward(
178
- image: DSPy::Image.from_file("path/to/image.jpg"),
179
- question: "What objects are visible in this image?"
180
- )
594
+ Add `gem 'async', '~> 2.29'` to the Gemfile. Handle errors within each `barrier.async` block to prevent one failure from cancelling others:
595
+
596
+ ```ruby
597
+ barrier.async do
598
+ begin
599
+ analyzer.call(content: doc)
600
+ rescue StandardError => e
601
+ nil
602
+ end
603
+ end
181
604
  ```
182
605
 
183
- ### Image Input Methods
606
+ ### Few-Shot Examples and Instruction Tuning
184
607
 
185
608
  ```ruby
186
- # From file path
187
- DSPy::Image.from_file("path/to/image.jpg")
609
+ classifier = DSPy::Predict.new(SentimentAnalysis)
188
610
 
189
- # From URL (OpenAI only)
190
- DSPy::Image.from_url("https://example.com/image.jpg")
611
+ examples = [
612
+ DSPy::FewShotExample.new(
613
+ input: { text: "Love it!" },
614
+ output: { sentiment: "positive", confidence: 0.95 }
615
+ )
616
+ ]
191
617
 
192
- # From base64-encoded data
193
- DSPy::Image.from_base64(base64_string, mime_type: "image/jpeg")
618
+ optimized = classifier.with_examples(examples)
619
+ tuned = classifier.with_instruction("Be precise and confident.")
194
620
  ```
195
621
 
196
- ## Best Practices
622
+ ---
197
623
 
198
- ### 1. Clear Signature Descriptions
624
+ ## Type System
199
625
 
200
- Always provide clear, specific descriptions for signatures and fields:
626
+ ### Automatic Type Conversion
201
627
 
202
- ```ruby
203
- # Good
204
- description "Classify customer support emails into Technical, Billing, or General categories"
205
-
206
- # Avoid
207
- description "Classify emails"
208
- ```
628
+ DSPy.rb v0.9.0+ automatically converts LLM JSON responses to typed Ruby objects:
209
629
 
210
- ### 2. Type Safety
630
+ - **Enums**: String values become `T::Enum` instances (case-insensitive)
631
+ - **Structs**: Nested hashes become `T::Struct` objects
632
+ - **Arrays**: Elements convert recursively
633
+ - **Defaults**: Missing fields use declared defaults
211
634
 
212
- Use specific types rather than generic String when possible:
635
+ ### Discriminators for Union Types
213
636
 
214
- ```ruby
215
- # Good - Use enums for constrained outputs
216
- output do
217
- const :category, T.enum(["Technical", "Billing", "General"])
218
- end
637
+ When a field uses `T.any()` with struct types, DSPy adds a `_type` field to each struct's schema. On deserialization, `_type` selects the correct struct class:
219
638
 
220
- # Less ideal - Generic string
221
- output do
222
- const :category, String, desc: "Must be Technical, Billing, or General"
223
- end
639
+ ```json
640
+ {
641
+ "action": {
642
+ "_type": "CreateTask",
643
+ "title": "Review Q4 Report"
644
+ }
645
+ }
224
646
  ```
225
647
 
226
- ### 3. Composable Architecture
648
+ DSPy matches `"CreateTask"` against the union members and instantiates the correct struct. No manual discriminator field is needed.
227
649
 
228
- Build complex workflows from simple, reusable modules:
650
+ ### Recursive Types
229
651
 
230
- ```ruby
231
- class EmailPipeline < DSPy::Module
232
- def initialize
233
- super
234
- @classifier = EmailClassifier.new
235
- @prioritizer = EmailPrioritizer.new
236
- @responder = EmailResponder.new
237
- end
652
+ Structs referencing themselves are supported. The schema generator tracks visited types and produces `$ref` pointers under `$defs`:
238
653
 
239
- def forward(email)
240
- classification = @classifier.forward(email)
241
- priority = @prioritizer.forward(classification)
242
- @responder.forward(classification.merge(priority))
243
- end
654
+ ```ruby
655
+ class TreeNode < T::Struct
656
+ const :label, String
657
+ const :children, T::Array[TreeNode], default: []
244
658
  end
245
659
  ```
246
660
 
247
- ### 4. Error Handling
661
+ The generated schema uses `"$ref": "#/$defs/TreeNode"` for the children array items, preventing infinite schema expansion.
248
662
 
249
- Always handle potential type validation errors:
663
+ ### Nesting Depth
250
664
 
251
- ```ruby
252
- begin
253
- result = predictor.forward(input_data)
254
- rescue DSPy::ValidationError => e
255
- # Handle validation error
256
- logger.error "Invalid output from LLM: #{e.message}"
257
- end
258
- ```
665
+ - 1-2 levels: reliable across all providers.
666
+ - 3-4 levels: works but increases schema complexity.
667
+ - 5+ levels: may trigger OpenAI depth validation warnings and reduce LLM accuracy. Flatten deeply nested structures or split into multiple signatures.
259
668
 
260
- ## Limitations
669
+ ### Tips
261
670
 
262
- Current constraints to be aware of:
263
- - No streaming support (single-request processing only)
264
- - Limited multimodal support through Ollama for local deployments
265
- - Vision capabilities vary by provider (see providers.md for compatibility matrix)
671
+ - Prefer `T::Array[X], default: []` over `T.nilable(T::Array[X])` -- the nilable form causes schema issues with OpenAI structured outputs.
672
+ - Use clear struct names for union types since they become `_type` discriminator values.
673
+ - Limit union types to 2-4 members for reliable model comprehension.
674
+ - Check schema compatibility with `DSPy::OpenAI::LM::SchemaConverter.validate_compatibility(schema)`.