roast-ai 0.4.9 → 0.5.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.
Files changed (194) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/docs/write-comments.md +36 -0
  3. data/.github/CODEOWNERS +1 -1
  4. data/.github/workflows/ci.yaml +10 -6
  5. data/.gitignore +0 -1
  6. data/.rubocop.yml +7 -1
  7. data/CLAUDE.md +2 -2
  8. data/CONTRIBUTING.md +2 -0
  9. data/Gemfile +18 -18
  10. data/Gemfile.lock +46 -57
  11. data/README.md +118 -1432
  12. data/README_LEGACY.md +1464 -0
  13. data/Rakefile +39 -4
  14. data/dev.yml +29 -0
  15. data/dsl/agent_sessions.rb +20 -0
  16. data/dsl/async_cogs.rb +49 -0
  17. data/dsl/async_cogs_complex.rb +67 -0
  18. data/dsl/call.rb +44 -0
  19. data/dsl/collect_from.rb +72 -0
  20. data/dsl/demo/Gemfile +4 -0
  21. data/dsl/demo/Gemfile.lock +120 -0
  22. data/dsl/demo/cogs/local.rb +15 -0
  23. data/dsl/demo/simple_external_cog.rb +17 -0
  24. data/dsl/json_output.rb +28 -0
  25. data/dsl/map.rb +55 -0
  26. data/dsl/map_reduce.rb +37 -0
  27. data/dsl/map_with_index.rb +49 -0
  28. data/dsl/next_break.rb +40 -0
  29. data/dsl/next_break_parallel.rb +44 -0
  30. data/dsl/outputs.rb +39 -0
  31. data/dsl/outputs_bang.rb +36 -0
  32. data/dsl/parallel_map.rb +37 -0
  33. data/dsl/plugin-gem-example/.gitignore +8 -0
  34. data/dsl/plugin-gem-example/Gemfile +13 -0
  35. data/dsl/plugin-gem-example/Gemfile.lock +178 -0
  36. data/dsl/plugin-gem-example/lib/other.rb +17 -0
  37. data/dsl/plugin-gem-example/lib/plugin_gem_example.rb +5 -0
  38. data/dsl/plugin-gem-example/lib/simple.rb +15 -0
  39. data/dsl/plugin-gem-example/lib/version.rb +10 -0
  40. data/dsl/plugin-gem-example/plugin-gem-example.gemspec +28 -0
  41. data/dsl/prompts/simple_prompt.md.erb +3 -0
  42. data/dsl/prototype.rb +10 -4
  43. data/dsl/repeat_loop_results.rb +53 -0
  44. data/dsl/ruby_cog.rb +72 -0
  45. data/dsl/simple_agent.rb +18 -0
  46. data/dsl/simple_chat.rb +26 -0
  47. data/dsl/simple_repeat.rb +29 -0
  48. data/dsl/skip.rb +36 -0
  49. data/dsl/step_communication.rb +10 -5
  50. data/dsl/targets_and_params.rb +57 -0
  51. data/dsl/temperature.rb +17 -0
  52. data/dsl/temporary_directory.rb +22 -0
  53. data/dsl/tutorial/01_your_first_workflow/README.md +179 -0
  54. data/dsl/tutorial/01_your_first_workflow/configured_chat.rb +33 -0
  55. data/dsl/tutorial/01_your_first_workflow/hello.rb +23 -0
  56. data/dsl/tutorial/02_chaining_cogs/README.md +310 -0
  57. data/dsl/tutorial/02_chaining_cogs/code_review.rb +104 -0
  58. data/dsl/tutorial/02_chaining_cogs/session_resumption.rb +92 -0
  59. data/dsl/tutorial/02_chaining_cogs/simple_chain.rb +84 -0
  60. data/dsl/tutorial/03_targets_and_params/README.md +230 -0
  61. data/dsl/tutorial/03_targets_and_params/multiple_targets.rb +65 -0
  62. data/dsl/tutorial/03_targets_and_params/single_target.rb +65 -0
  63. data/dsl/tutorial/04_configuration_options/README.md +209 -0
  64. data/dsl/tutorial/04_configuration_options/control_display_and_temperature.rb +104 -0
  65. data/dsl/tutorial/04_configuration_options/simple_config.rb +68 -0
  66. data/dsl/tutorial/05_control_flow/README.md +156 -0
  67. data/dsl/tutorial/05_control_flow/conditional_execution.rb +62 -0
  68. data/dsl/tutorial/05_control_flow/handling_failures.rb +77 -0
  69. data/dsl/tutorial/06_reusable_scopes/README.md +172 -0
  70. data/dsl/tutorial/06_reusable_scopes/accessing_scope_outputs.rb +126 -0
  71. data/dsl/tutorial/06_reusable_scopes/basic_scope.rb +63 -0
  72. data/dsl/tutorial/06_reusable_scopes/parameterized_scope.rb +78 -0
  73. data/dsl/tutorial/07_processing_collections/README.md +152 -0
  74. data/dsl/tutorial/07_processing_collections/basic_map.rb +70 -0
  75. data/dsl/tutorial/07_processing_collections/parallel_map.rb +74 -0
  76. data/dsl/tutorial/08_iterative_workflows/README.md +231 -0
  77. data/dsl/tutorial/08_iterative_workflows/basic_repeat.rb +57 -0
  78. data/dsl/tutorial/08_iterative_workflows/conditional_break.rb +57 -0
  79. data/dsl/tutorial/09_async_cogs/README.md +197 -0
  80. data/dsl/tutorial/09_async_cogs/basic_async.rb +38 -0
  81. data/dsl/tutorial/README.md +222 -0
  82. data/dsl/working_directory.rb +16 -0
  83. data/exe/roast +1 -1
  84. data/internal/documentation/architectural-notes.md +115 -0
  85. data/internal/documentation/doc-comments-external.md +686 -0
  86. data/internal/documentation/doc-comments-internal.md +342 -0
  87. data/internal/documentation/doc-comments.md +211 -0
  88. data/lib/roast/dsl/cog/config.rb +280 -4
  89. data/lib/roast/dsl/cog/input.rb +73 -0
  90. data/lib/roast/dsl/cog/output.rb +313 -0
  91. data/lib/roast/dsl/cog/registry.rb +71 -0
  92. data/lib/roast/dsl/cog/stack.rb +3 -2
  93. data/lib/roast/dsl/cog/store.rb +11 -8
  94. data/lib/roast/dsl/cog.rb +108 -31
  95. data/lib/roast/dsl/cog_input_context.rb +44 -0
  96. data/lib/roast/dsl/cog_input_manager.rb +156 -0
  97. data/lib/roast/dsl/cogs/agent/config.rb +465 -0
  98. data/lib/roast/dsl/cogs/agent/input.rb +81 -0
  99. data/lib/roast/dsl/cogs/agent/output.rb +59 -0
  100. data/lib/roast/dsl/cogs/agent/provider.rb +51 -0
  101. data/lib/roast/dsl/cogs/agent/providers/claude/claude_invocation.rb +185 -0
  102. data/lib/roast/dsl/cogs/agent/providers/claude/message.rb +73 -0
  103. data/lib/roast/dsl/cogs/agent/providers/claude/messages/assistant_message.rb +36 -0
  104. data/lib/roast/dsl/cogs/agent/providers/claude/messages/result_message.rb +61 -0
  105. data/lib/roast/dsl/cogs/agent/providers/claude/messages/system_message.rb +47 -0
  106. data/lib/roast/dsl/cogs/agent/providers/claude/messages/text_message.rb +36 -0
  107. data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_result_message.rb +47 -0
  108. data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_use_message.rb +46 -0
  109. data/lib/roast/dsl/cogs/agent/providers/claude/messages/unknown_message.rb +27 -0
  110. data/lib/roast/dsl/cogs/agent/providers/claude/messages/user_message.rb +37 -0
  111. data/lib/roast/dsl/cogs/agent/providers/claude/tool_result.rb +51 -0
  112. data/lib/roast/dsl/cogs/agent/providers/claude/tool_use.rb +48 -0
  113. data/lib/roast/dsl/cogs/agent/providers/claude.rb +31 -0
  114. data/lib/roast/dsl/cogs/agent/stats.rb +92 -0
  115. data/lib/roast/dsl/cogs/agent/usage.rb +62 -0
  116. data/lib/roast/dsl/cogs/agent.rb +75 -0
  117. data/lib/roast/dsl/cogs/chat/config.rb +453 -0
  118. data/lib/roast/dsl/cogs/chat/input.rb +92 -0
  119. data/lib/roast/dsl/cogs/chat/output.rb +64 -0
  120. data/lib/roast/dsl/cogs/chat/session.rb +68 -0
  121. data/lib/roast/dsl/cogs/chat.rb +81 -0
  122. data/lib/roast/dsl/cogs/cmd.rb +291 -27
  123. data/lib/roast/dsl/cogs/ruby.rb +171 -0
  124. data/lib/roast/dsl/command_runner.rb +191 -0
  125. data/lib/roast/dsl/config_context.rb +2 -47
  126. data/lib/roast/dsl/config_manager.rb +143 -0
  127. data/lib/roast/dsl/control_flow.rb +41 -0
  128. data/lib/roast/dsl/execution_context.rb +9 -0
  129. data/lib/roast/dsl/execution_manager.rb +267 -0
  130. data/lib/roast/dsl/nil_assertions.rb +23 -0
  131. data/lib/roast/dsl/system_cog/params.rb +32 -0
  132. data/lib/roast/dsl/system_cog.rb +36 -0
  133. data/lib/roast/dsl/system_cogs/call.rb +162 -0
  134. data/lib/roast/dsl/system_cogs/map.rb +448 -0
  135. data/lib/roast/dsl/system_cogs/repeat.rb +242 -0
  136. data/lib/roast/dsl/workflow.rb +123 -0
  137. data/lib/roast/dsl/workflow_context.rb +20 -0
  138. data/lib/roast/dsl/workflow_params.rb +24 -0
  139. data/lib/roast/sorbet_runtime_stub.rb +154 -0
  140. data/lib/roast/tools/apply_diff.rb +1 -3
  141. data/lib/roast/tools/cmd.rb +4 -3
  142. data/lib/roast/tools/read_file.rb +1 -1
  143. data/lib/roast/tools/update_files.rb +1 -1
  144. data/lib/roast/tools/write_file.rb +1 -1
  145. data/lib/roast/version.rb +1 -1
  146. data/lib/roast/workflow/base_workflow.rb +4 -0
  147. data/lib/roast/workflow/step_loader.rb +14 -2
  148. data/lib/roast-ai.rb +4 -0
  149. data/lib/roast.rb +60 -22
  150. data/{roast.gemspec → roast-ai.gemspec} +10 -13
  151. data/sorbet/config +1 -0
  152. data/sorbet/rbi/gems/async@2.34.0.rbi +1577 -0
  153. data/sorbet/rbi/gems/cli-kit@5.2.0.rbi +2063 -0
  154. data/sorbet/rbi/gems/{cli-ui@2.3.0.rbi → cli-ui@2.7.0-6bdefd1d06305e5d6ae312ac76f9c88f88658dda.rbi} +1418 -1013
  155. data/sorbet/rbi/gems/console@1.34.2.rbi +1193 -0
  156. data/sorbet/rbi/gems/fiber-annotation@0.2.0.rbi +50 -0
  157. data/sorbet/rbi/gems/fiber-local@1.1.0.rbi +35 -0
  158. data/sorbet/rbi/gems/fiber-storage@1.0.1.rbi +41 -0
  159. data/sorbet/rbi/gems/io-event@1.14.0.rbi +724 -0
  160. data/sorbet/rbi/gems/marcel@1.1.0.rbi +239 -0
  161. data/sorbet/rbi/gems/metrics@0.15.0.rbi +9 -0
  162. data/sorbet/rbi/gems/ruby_llm@1.8.2.rbi +5703 -0
  163. data/sorbet/rbi/gems/traces@0.18.2.rbi +9 -0
  164. data/sorbet/rbi/shims/lib/roast/dsl/cog_input_context.rbi +1197 -0
  165. data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +314 -2
  166. data/sorbet/rbi/shims/lib/roast/dsl/execution_context.rbi +498 -0
  167. data/sorbet/tapioca/config.yml +6 -0
  168. data/sorbet/tapioca/require.rb +2 -0
  169. metadata +198 -34
  170. data/dsl/less_simple.rb +0 -112
  171. data/dsl/simple.rb +0 -8
  172. data/lib/roast/dsl/cog_execution_context.rb +0 -29
  173. data/lib/roast/dsl/cogs/graph.rb +0 -53
  174. data/lib/roast/dsl/cogs.rb +0 -65
  175. data/lib/roast/dsl/executor.rb +0 -82
  176. data/lib/roast/dsl/workflow_execution_context.rb +0 -47
  177. data/sorbet/rbi/gems/cgi@0.5.0.rbi +0 -2961
  178. data/sorbet/rbi/gems/claude_swarm@0.1.19.rbi +0 -568
  179. data/sorbet/rbi/gems/cli-kit@5.0.1.rbi +0 -1991
  180. data/sorbet/rbi/gems/dry-configurable@1.3.0.rbi +0 -672
  181. data/sorbet/rbi/gems/dry-core@1.1.0.rbi +0 -1894
  182. data/sorbet/rbi/gems/dry-inflector@1.2.0.rbi +0 -659
  183. data/sorbet/rbi/gems/dry-initializer@3.2.0.rbi +0 -781
  184. data/sorbet/rbi/gems/dry-logic@1.6.0.rbi +0 -1127
  185. data/sorbet/rbi/gems/dry-schema@1.14.1.rbi +0 -3727
  186. data/sorbet/rbi/gems/dry-types@1.8.3.rbi +0 -3969
  187. data/sorbet/rbi/gems/fast-mcp-annotations@1.5.3.rbi +0 -1588
  188. data/sorbet/rbi/gems/mime-types-data@3.2025.0617.rbi +0 -136
  189. data/sorbet/rbi/gems/mime-types@3.7.0.rbi +0 -1342
  190. data/sorbet/rbi/gems/rack@2.2.18.rbi +0 -5659
  191. data/sorbet/rbi/gems/rbs-inline@0.12.0.rbi +0 -2170
  192. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
  193. data/sorbet/rbi/gems/yard@0.9.37.rbi +0 -18492
  194. data/sorbet/rbi/shims/lib/roast/dsl/workflow_execution_context.rbi +0 -11
@@ -0,0 +1,63 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ # This workflow demonstrates basic named execute scopes and the call cog.
7
+ # It shows how to define reusable logic and invoke it multiple times.
8
+
9
+ config {}
10
+
11
+ # Define a reusable scope that displays a separator
12
+ execute(:print_separator) do
13
+ ruby { puts "=" * 70 }
14
+ end
15
+
16
+ # Define a scope that generates a random word
17
+ execute(:random_word) do
18
+ cmd(:word) { "shuf /usr/share/dict/words -n 1" }
19
+
20
+ outputs! { cmd!(:word).text }
21
+ end
22
+
23
+ # Main workflow
24
+ execute do
25
+ call(run: :print_separator)
26
+
27
+ ruby { puts "REUSABLE SCOPES EXAMPLE" }
28
+
29
+ call(run: :print_separator)
30
+
31
+ # Call the random_word scope three times
32
+ ruby { puts "\nGenerating three random words:" }
33
+
34
+ call(:word1, run: :random_word)
35
+ call(:word2, run: :random_word)
36
+ call(:word3, run: :random_word)
37
+
38
+ # Extract and display the results
39
+ ruby do
40
+ word1 = from(call!(:word1))
41
+ word2 = from(call!(:word2))
42
+ word3 = from(call!(:word3))
43
+
44
+ puts " 1. #{word1}"
45
+ puts " 2. #{word2}"
46
+ puts " 3. #{word3}"
47
+ end
48
+
49
+ call(run: :print_separator)
50
+
51
+ ruby do
52
+ puts <<~NOTE
53
+ KEY POINTS:
54
+ - Named scopes don't run automatically - they must be called
55
+ - The same scope can be called multiple times
56
+ - Each call is independent with its own execution context
57
+ - Use outputs! to return values from a scope
58
+ - Use from() to extract the returned value
59
+ NOTE
60
+ end
61
+
62
+ call(run: :print_separator)
63
+ end
@@ -0,0 +1,78 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ # This workflow demonstrates parameterized scopes.
7
+ # It shows how to pass values to scopes and use them within the scope's cogs.
8
+
9
+ config do
10
+ chat do
11
+ model "gpt-4o-mini"
12
+ end
13
+ end
14
+
15
+ # Define a scope that analyzes text
16
+ execute(:analyze_text) do
17
+ chat(:analysis) do |_, text|
18
+ # The text parameter comes from the call cog's input block
19
+ <<~PROMPT
20
+ Analyze this text and provide:
21
+ 1. Word count (approximate)
22
+ 2. Main topic
23
+ 3. Sentiment (positive/negative/neutral)
24
+
25
+ Text: #{text}
26
+
27
+ Return your analysis as a brief summary.
28
+ PROMPT
29
+ end
30
+
31
+ outputs! { chat!(:analysis).response }
32
+ end
33
+
34
+ # Main workflow
35
+ execute do
36
+ ruby { puts "=" * 70 }
37
+ ruby { puts "PARAMETERIZED SCOPES EXAMPLE" }
38
+ ruby { puts "=" * 70 }
39
+
40
+ # Analyze different texts using the same scope
41
+ call(:analysis1, run: :analyze_text) do
42
+ "The quick brown fox jumps over the lazy dog."
43
+ end
44
+
45
+ call(:analysis2, run: :analyze_text) do
46
+ "Artificial intelligence is transforming how we build software."
47
+ end
48
+
49
+ call(:analysis3, run: :analyze_text) do
50
+ "I'm disappointed with the results of this experiment."
51
+ end
52
+
53
+ # Display all results
54
+ ruby do
55
+ puts "\nANALYSIS 1:"
56
+ puts from(call!(:analysis1))
57
+
58
+ puts "\nANALYSIS 2:"
59
+ puts from(call!(:analysis2))
60
+
61
+ puts "\nANALYSIS 3:"
62
+ puts from(call!(:analysis3))
63
+ end
64
+
65
+ ruby { puts "\n" + "=" * 70 }
66
+
67
+ ruby do
68
+ puts <<~NOTE
69
+ KEY POINTS:
70
+ - Pass values to scopes using call's input block
71
+ - Access the value as the second parameter in cog blocks
72
+ - The same scope can process different inputs
73
+ - This makes scopes reusable and composable
74
+ NOTE
75
+ end
76
+
77
+ ruby { puts "=" * 70 }
78
+ end
@@ -0,0 +1,152 @@
1
+ # Chapter 7: Processing Collections
2
+
3
+ Learn how to process arrays and other collections with the `map` cog, which applies a named execute scope to each item
4
+ in a collection.
5
+
6
+ ## The `map` Cog
7
+
8
+ The `map` cog executes a named execute scope (defined with `execute(:name)`) for each item in a collection:
9
+
10
+ ```ruby
11
+ execute(:process_item) do
12
+ chat(:analyze) do |_, item|
13
+ "Analyze this item: #{item}"
14
+ end
15
+ end
16
+
17
+ execute do
18
+ map(run: :process_item) { ["item1", "item2", "item3"] }
19
+ end
20
+ ```
21
+
22
+ The scope receives each item as its value parameter, just like with the `call` cog.
23
+
24
+ ## Collecting Results
25
+
26
+ Use `collect` to gather outputs from all iterations into an array:
27
+
28
+ ```ruby
29
+ # Get all final outputs
30
+ results = collect(map!(:process_items))
31
+
32
+ # Transform outputs with a block, with access to the original item and its index in the source collection
33
+ results = collect(map!(:process_items)) do |output, item, index|
34
+ { original_item: item, result_text: output.text, index: }
35
+ end
36
+
37
+ # Access other cog outputs from within each iteration
38
+ sessions = collect(map!(:process_items)) do
39
+ chat!(:analyze).session
40
+ end
41
+ ```
42
+
43
+ ## Reducing Results
44
+
45
+ Use `reduce` to combine outputs into a single value:
46
+
47
+ ```ruby
48
+ # Sum all outputs
49
+ # The second argument to `reduce` is the initial value for the accumulator. It is required.
50
+ total = reduce(map!(:calculate_scores), 0) do |sum, output|
51
+ # the block given to `reduce` should return the new value of the accumulator at each step
52
+ sum + output
53
+ end
54
+
55
+ # Build a hash
56
+ results = reduce(map!(:process_items), {}) do |hash, output, item, index|
57
+ # returning nil will skip this item; it will not reset the accumulator to nil
58
+ hash.merge(item => output) unless output.text.include?("failure")
59
+ end
60
+ ```
61
+
62
+ ## Parallel Execution
63
+
64
+ By default, `map` runs iterations serially. Configure parallel execution for the `map` cog in the `config` block.
65
+ Roast uses fibers for efficient asynchronous operation.
66
+
67
+ ```ruby
68
+ config do
69
+ map do
70
+ parallel(3) # Run up to 3 iterations concurrently for all `map` cogs
71
+ end
72
+
73
+ map(:unlimited) do
74
+ parallel! # Run all iterations in parallel for a specific `map` cog
75
+ end
76
+ end
77
+ ```
78
+
79
+ Results from `collect` and `reduce` are always returned in the original order, regardless of completion order.
80
+
81
+ ## Accessing Specific Iterations
82
+
83
+ Access the output from a specific iteration using `.iteration(index)`.
84
+ This is convenient shorthand to avoid having to `collect` all the outputs when you only want one
85
+ in a particular situation.
86
+
87
+ ```ruby
88
+ # Get output from third iteration (index 2)
89
+ result = from(map!(:process_items).iteration(2))
90
+
91
+ # Access first and last iterations
92
+ first_result = from(map!(:process_items).first)
93
+ last_result = from(map!(:process_items).last)
94
+
95
+ # Use with a block to access specific cogs
96
+ result = from(map!(:process_items).iteration(2)) do
97
+ chat!(:analyze).response
98
+ end
99
+ ```
100
+
101
+ ## Working with Indices
102
+
103
+ Scopes called by `map` receive the iteration index as a third parameter:
104
+
105
+ ```ruby
106
+ execute(:process_with_index) do
107
+ chat(:numbered) do |_, item, index|
108
+ "Process item #{index}: #{item}"
109
+ end
110
+ end
111
+
112
+ execute do
113
+ # Default: indices start at 0
114
+ map(run: :process_with_index) { ["a", "b", "c"] }
115
+
116
+ # Custom starting index
117
+ map(run: :process_with_index) do |my|
118
+ my.items = ["a", "b", "c"]
119
+ my.initial_index = 1 # Start counting from 1
120
+ end
121
+ end
122
+ ```
123
+
124
+ Technically, because you can call a scope with `call` just as well as `map`, the third `index` argument
125
+ will always be present. When you invoke a scope with `call`, the index will be 0 by default. You can also override
126
+ the index value for an individual call invocation, to simulate processing one item from the middle of a collection.
127
+
128
+ ```ruby
129
+ call(run: :some_scope) do |my|
130
+ my.value = "some data"
131
+ my.index = 3
132
+ end
133
+ ```
134
+
135
+ ## Running the Workflows
136
+
137
+ Try these examples to see `map` in action:
138
+
139
+ ```bash
140
+ # Basic map with collect, reduce, and accessing specific iterations
141
+ bin/roast execute --executor=dsl dsl/tutorial/07_processing_collections/basic_map.rb
142
+
143
+ # Parallel execution
144
+ bin/roast execute --executor=dsl dsl/tutorial/07_processing_collections/parallel_map.rb
145
+ ```
146
+
147
+ ## What's Next?
148
+
149
+ In the next chapter, you'll learn about iterative workflows with the `repeat` cog: an easy way to execute a set of
150
+ steps repeatedly until a condition is met.
151
+
152
+ But first, experiment with `map` to process collections and try different parallel execution strategies!
@@ -0,0 +1,70 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ config do
7
+ chat do
8
+ model "gpt-4o-mini"
9
+ provider :openai
10
+ no_display!
11
+ end
12
+ end
13
+
14
+ execute(:analyze_city) do
15
+ chat(:extract_info) do |_, city|
16
+ <<~PROMPT
17
+ For the city #{city}, provide a one-sentence fact about its population or geography.
18
+ Keep it brief and factual.
19
+ PROMPT
20
+ end
21
+ end
22
+
23
+ execute do
24
+ cities = ["Tokyo", "Paris", "Cairo", "Sydney"]
25
+
26
+ # Apply the :analyze_city scope to each city
27
+ map(:city_analysis, run: :analyze_city) { cities }
28
+
29
+ # Collect all the responses
30
+ ruby do
31
+ puts "\n=== City Facts ==="
32
+ facts = collect(map!(:city_analysis)) do
33
+ chat!(:extract_info).response.strip
34
+ end
35
+
36
+ cities.each_with_index do |city, i|
37
+ puts "#{city}: #{facts[i]}"
38
+ end
39
+ end
40
+
41
+ # Use reduce to combine results
42
+ ruby do
43
+ summary = reduce(map!(:city_analysis), "Summary of cities:") do |acc, _, city, index|
44
+ "#{acc}\n- #{city} (position #{index})"
45
+ end
46
+ puts "\n#{summary}"
47
+ end
48
+
49
+ # Access a specific iteration
50
+ ruby do
51
+ puts "\n=== Accessing Specific Iterations ==="
52
+
53
+ # Get the second city's analysis (Paris, index 1)
54
+ paris_fact = from(map!(:city_analysis).iteration(1)) do
55
+ chat!(:extract_info).response.strip
56
+ end
57
+ puts "Paris (index 1): #{paris_fact}"
58
+
59
+ # Access first and last
60
+ first_city = from(map!(:city_analysis).first) do
61
+ chat!(:extract_info).response.strip
62
+ end
63
+ last_city = from(map!(:city_analysis).last) do
64
+ chat!(:extract_info).response.strip
65
+ end
66
+
67
+ puts "First: #{first_city}"
68
+ puts "Last: #{last_city}"
69
+ end
70
+ end
@@ -0,0 +1,74 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ config do
7
+ chat do
8
+ model "gpt-4o-mini"
9
+ provider :openai
10
+ no_display!
11
+ end
12
+
13
+ map(:serial) do
14
+ no_parallel! # Explicitly serial (this is the default)
15
+ end
16
+
17
+ map(:limited_parallel) do
18
+ parallel(2) # Process up to 2 items concurrently
19
+ end
20
+
21
+ map(:unlimited_parallel) do
22
+ parallel! # Process all items concurrently
23
+ end
24
+ end
25
+
26
+ execute(:generate_fact) do
27
+ chat(:fact) do |_, topic, index|
28
+ <<~PROMPT
29
+ Generate a brief, interesting fact (one sentence) about: #{topic}
30
+ Label it as "Fact #{index}:"
31
+ PROMPT
32
+ end
33
+ end
34
+
35
+ execute do
36
+ topics = ["Ruby", "Python", "JavaScript", "Go", "Rust"]
37
+
38
+ # Serial execution (one at a time)
39
+ map(:serial, run: :generate_fact) do |my|
40
+ my.items = topics
41
+ my.initial_index = 1
42
+ end
43
+
44
+ ruby do
45
+ puts "\n=== Serial Execution ==="
46
+ facts = collect(map!(:serial)) { chat!(:fact).response.strip }
47
+ facts.each { |fact| puts fact }
48
+ end
49
+
50
+ # Limited parallelism (up to 2 concurrent)
51
+ map(:limited_parallel, run: :generate_fact) do |my|
52
+ my.items = topics
53
+ my.initial_index = 1
54
+ end
55
+
56
+ ruby do
57
+ puts "\n=== Limited Parallel Execution (max 2 concurrent) ==="
58
+ facts = collect(map!(:limited_parallel)) { chat!(:fact).response.strip }
59
+ facts.each { |fact| puts fact }
60
+ end
61
+
62
+ # Unlimited parallelism (all at once)
63
+ map(:unlimited_parallel, run: :generate_fact) do |my|
64
+ my.items = topics
65
+ my.initial_index = 1
66
+ end
67
+
68
+ ruby do
69
+ puts "\n=== Unlimited Parallel Execution ==="
70
+ facts = collect(map!(:unlimited_parallel)) { chat!(:fact).response.strip }
71
+ facts.each { |fact| puts fact }
72
+ puts "\nNote: Results are always returned in original order, regardless of completion order."
73
+ end
74
+ end
@@ -0,0 +1,231 @@
1
+ # Chapter 8: Iterative Workflows
2
+
3
+ Learn how to create iterative workflows with the `repeat` cog, which executes a named scope repeatedly until a condition
4
+ is met. Unlike `map` which processes a collection holding a fix number of items, `repeat` continues indefinitely until
5
+ it hits a specified maximum number of iterations or you explicitly stop it with `break!`.
6
+
7
+ ## The `repeat` Cog
8
+
9
+ The `repeat` cog executes a named execute scope repeatedly, where the output of each iteration becomes the input to the
10
+ next:
11
+
12
+ ```ruby
13
+ execute(:process) do
14
+ ruby(:step_increment) do |_, value, index|
15
+ new_value = value + index
16
+ puts "Current value: #{new_value}"
17
+ new_value
18
+ end
19
+
20
+ ruby { break! if ruby!(:step_increment).value >= 12 }
21
+
22
+ outputs { ruby!(:step_increment).value }
23
+ end
24
+
25
+ execute do
26
+ repeat(:loop, run: :process) { 0 } # Start with initial value 0
27
+
28
+ ruby do
29
+ puts "Final value: #{repeat!(:loop).value}"
30
+ end
31
+ end
32
+ ```
33
+
34
+ Each iteration receives two parameters:
35
+
36
+ 1. The **value** from the previous iteration (or the initial value for the first iteration)
37
+ 2. The **index** starting at 0 (or a custom `initial_index`)
38
+
39
+ Key differences from `map`:
40
+
41
+ - `repeat` continues until you call `break!` (or hit the specified maximum number of iterations)
42
+ - Each iteration's output becomes the next iteration's input (iterative transformation)
43
+ - `map` processes independent items; `repeat` builds up a result iteratively
44
+
45
+ ## Maximum Iteration Limit
46
+
47
+ You can specify the optional `max_iterations` attribute on the input value of the `repeat` cog to set a limit
48
+ on the number of loop iterations that can run. By default, the value of this attribute is `nil`, and the loop
49
+ will be allowed to run forever, until explicitly terminated from within.
50
+
51
+ ## Breaking Out of Loops
52
+
53
+ Use `break!` to terminate the loop. Once `break!` is called, no more iterations will run.
54
+ Also, the cog in which you call `break!` will not run, and no subsequent cogs in that iteration will run either.
55
+ It is often cleanest to call `break!` in a `ruby` cog at the end of the scope, that only performs a check for the
56
+ termination condition(s), but you can call it anywhere that makes sense.
57
+
58
+ NOTE: the `outputs` block will __always__ run, even if you call `break!` earlier in the scope.
59
+
60
+ ```ruby
61
+ execute(:find_threshold) do
62
+ chat(:analyze) do |_, number|
63
+ "Is #{number} greater than 50? Answer yes or no."
64
+ end
65
+
66
+ ruby do
67
+ response = chat!(:analyze).response
68
+ break! if response.downcase.include?("yes")
69
+ end
70
+
71
+ outputs { |_, number| number + 10 }
72
+ end
73
+
74
+ execute do
75
+ repeat(:search, run: :find_threshold) { 0 }
76
+
77
+ ruby do
78
+ puts "Stopped at: #{repeat!(:search).value}"
79
+ end
80
+ end
81
+ ```
82
+
83
+ ## Skipping to the Next Iteration
84
+
85
+ Use `next!` to skip the rest of the current iteration and immediately start the next one:
86
+
87
+ ```ruby
88
+ execute(:process_numbers) do
89
+ ruby(:check) do |_, _, index|
90
+ # Skip processing for multiples of 3
91
+ if index % 3 == 0
92
+ puts " Skipping every third 3 iteration"
93
+ next!
94
+ end
95
+ puts "Processing #{index}"
96
+ end
97
+
98
+ ruby(:double) do |_, value|
99
+ result = value * 2
100
+ puts " Doubled to #{result}"
101
+ result
102
+ end
103
+
104
+ ruby { |_, _, index| break! if index >= 10 }
105
+
106
+ outputs { ruby!(:double).value }
107
+ end
108
+
109
+ execute do
110
+ repeat(run: :process_numbers) { 1 }
111
+ end
112
+ ```
113
+
114
+ When `next!` is called, the remaining cogs in that iteration are skipped, but the `outputs` block will still run, to
115
+ generate the input value for the next iteration.
116
+
117
+ ## Accessing Iteration Results
118
+
119
+ Access specific iterations or the final result:
120
+
121
+ ```ruby
122
+ execute do
123
+ repeat(:loop, run: :process) { 0 }
124
+
125
+ ruby do
126
+ # Get value directly from the final iteration's output block
127
+ final = repeat!(:loop).value
128
+ puts "Final result: #{final}"
129
+
130
+ # Access specific iterations
131
+ first = from(repeat!(:loop).first)
132
+ last = from(repeat!(:loop).last)
133
+ third = from(repeat!(:loop).iteration(2))
134
+
135
+ puts "First iteration: #{first}"
136
+ puts "Last iteration: #{last}"
137
+ puts "Third iteration: #{third}"
138
+
139
+ # Access specific cogs in specific iterations
140
+ answer = from(repeat!(:loop).iteration(-2)) { chat!(:question).text }
141
+ puts "Second-to-last answer: #{answer}"
142
+ end
143
+ end
144
+ ```
145
+
146
+ ## Processing All Iterations
147
+
148
+ Use `.results` with `collect` or `reduce` to process all iteration outputs:
149
+
150
+ ```ruby
151
+ execute do
152
+ repeat(:loop, run: :calculate) { 1 }
153
+
154
+ ruby do
155
+ # Collect all intermediate values
156
+ all_values = collect(repeat!(:loop).results) do
157
+ ruby!(:calculate).value
158
+ end
159
+ puts "All values: #{all_values.inspect}"
160
+
161
+ # Sum all iterations
162
+ total = reduce(repeat!(:loop).results, 0) do |sum|
163
+ sum + ruby!(:calculate).value
164
+ end
165
+ puts "Sum of all iterations: #{total}"
166
+ end
167
+ end
168
+ ```
169
+
170
+ ## The `outputs` Block
171
+
172
+ The `outputs` block determines what value gets passed to the next iteration. It always runs, even on an iteration where
173
+ `break!` or `next!` is called:
174
+
175
+ ```ruby
176
+ execute(:accumulate) do
177
+ ruby(:add) do |_, sum, index|
178
+ sum + index
179
+ end
180
+
181
+ ruby { |_, _, index| break! if index >= 5 }
182
+
183
+ # This runs even on the final iteration (when break! is called)
184
+ outputs { ruby!(:add).value }
185
+ end
186
+
187
+ execute do
188
+ repeat(run: :accumulate) { 0 }
189
+
190
+ ruby do
191
+ # The final value includes the computation from the iteration where break! was called
192
+ puts "Final sum: #{repeat!(:accumulate).value}"
193
+ end
194
+ end
195
+ ```
196
+
197
+ ## Custom Starting Index
198
+
199
+ Customize where iteration indices start:
200
+
201
+ ```ruby
202
+ execute do
203
+ repeat(run: :process) do |my|
204
+ my.value = 100
205
+ my.index = 10 # Start counting from 10
206
+ end
207
+
208
+ ruby do
209
+ puts "Final: #{repeat!.value}"
210
+ end
211
+ end
212
+ ```
213
+
214
+ ## Running the Workflows
215
+
216
+ Try these examples to see `repeat` in action:
217
+
218
+ ```bash
219
+ # Basic iterative transformation
220
+ bin/roast execute --executor=dsl dsl/tutorial/08_iterative_workflows/basic_repeat.rb
221
+
222
+ # Using break! to terminate based on conditions
223
+ bin/roast execute --executor=dsl dsl/tutorial/08_iterative_workflows/conditional_break.rb
224
+ ```
225
+
226
+ ## What's Next?
227
+
228
+ In the final chapter, you'll learn about asynchronous cogs: how to run multiple independent tasks concurrently to improve
229
+ workflow performance without waiting for each task to complete before starting the next.
230
+
231
+ But first, experiment with `repeat` to create iterative transformations and see how values flow from one iteration to the next!