roast-ai 0.4.10 → 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 (175) 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/.ruby-version +1 -1
  8. data/CLAUDE.md +2 -2
  9. data/CONTRIBUTING.md +2 -0
  10. data/Gemfile +19 -18
  11. data/Gemfile.lock +35 -58
  12. data/README.md +118 -1432
  13. data/README_LEGACY.md +1464 -0
  14. data/Rakefile +39 -4
  15. data/dev.yml +29 -0
  16. data/dsl/agent_sessions.rb +20 -0
  17. data/dsl/async_cogs.rb +49 -0
  18. data/dsl/async_cogs_complex.rb +67 -0
  19. data/dsl/call.rb +44 -0
  20. data/dsl/collect_from.rb +72 -0
  21. data/dsl/json_output.rb +28 -0
  22. data/dsl/map.rb +55 -0
  23. data/dsl/map_reduce.rb +37 -0
  24. data/dsl/map_with_index.rb +49 -0
  25. data/dsl/next_break.rb +45 -0
  26. data/dsl/next_break_parallel.rb +44 -0
  27. data/dsl/outputs.rb +39 -0
  28. data/dsl/outputs_bang.rb +36 -0
  29. data/dsl/parallel_map.rb +37 -0
  30. data/dsl/prompts/simple_prompt.md.erb +3 -0
  31. data/dsl/prototype.rb +5 -7
  32. data/dsl/repeat_loop_results.rb +53 -0
  33. data/dsl/ruby_cog.rb +72 -0
  34. data/dsl/simple_agent.rb +18 -0
  35. data/dsl/simple_chat.rb +15 -1
  36. data/dsl/simple_repeat.rb +29 -0
  37. data/dsl/skip.rb +36 -0
  38. data/dsl/step_communication.rb +2 -3
  39. data/dsl/targets_and_params.rb +57 -0
  40. data/dsl/temperature.rb +17 -0
  41. data/dsl/temporary_directory.rb +22 -0
  42. data/dsl/tutorial/01_your_first_workflow/README.md +179 -0
  43. data/dsl/tutorial/01_your_first_workflow/configured_chat.rb +33 -0
  44. data/dsl/tutorial/01_your_first_workflow/hello.rb +23 -0
  45. data/dsl/tutorial/02_chaining_cogs/README.md +310 -0
  46. data/dsl/tutorial/02_chaining_cogs/code_review.rb +104 -0
  47. data/dsl/tutorial/02_chaining_cogs/session_resumption.rb +92 -0
  48. data/dsl/tutorial/02_chaining_cogs/simple_chain.rb +84 -0
  49. data/dsl/tutorial/03_targets_and_params/README.md +230 -0
  50. data/dsl/tutorial/03_targets_and_params/multiple_targets.rb +65 -0
  51. data/dsl/tutorial/03_targets_and_params/single_target.rb +65 -0
  52. data/dsl/tutorial/04_configuration_options/README.md +209 -0
  53. data/dsl/tutorial/04_configuration_options/control_display_and_temperature.rb +104 -0
  54. data/dsl/tutorial/04_configuration_options/simple_config.rb +68 -0
  55. data/dsl/tutorial/05_control_flow/README.md +156 -0
  56. data/dsl/tutorial/05_control_flow/conditional_execution.rb +62 -0
  57. data/dsl/tutorial/05_control_flow/handling_failures.rb +77 -0
  58. data/dsl/tutorial/06_reusable_scopes/README.md +172 -0
  59. data/dsl/tutorial/06_reusable_scopes/accessing_scope_outputs.rb +126 -0
  60. data/dsl/tutorial/06_reusable_scopes/basic_scope.rb +63 -0
  61. data/dsl/tutorial/06_reusable_scopes/parameterized_scope.rb +78 -0
  62. data/dsl/tutorial/07_processing_collections/README.md +152 -0
  63. data/dsl/tutorial/07_processing_collections/basic_map.rb +70 -0
  64. data/dsl/tutorial/07_processing_collections/parallel_map.rb +74 -0
  65. data/dsl/tutorial/08_iterative_workflows/README.md +231 -0
  66. data/dsl/tutorial/08_iterative_workflows/basic_repeat.rb +57 -0
  67. data/dsl/tutorial/08_iterative_workflows/conditional_break.rb +57 -0
  68. data/dsl/tutorial/09_async_cogs/README.md +197 -0
  69. data/dsl/tutorial/09_async_cogs/basic_async.rb +38 -0
  70. data/dsl/tutorial/README.md +222 -0
  71. data/dsl/working_directory.rb +16 -0
  72. data/exe/roast +1 -1
  73. data/internal/documentation/architectural-notes.md +115 -0
  74. data/internal/documentation/doc-comments-external.md +686 -0
  75. data/internal/documentation/doc-comments-internal.md +342 -0
  76. data/internal/documentation/doc-comments.md +211 -0
  77. data/lib/roast/dsl/cog/config.rb +274 -3
  78. data/lib/roast/dsl/cog/input.rb +53 -10
  79. data/lib/roast/dsl/cog/output.rb +297 -8
  80. data/lib/roast/dsl/cog/registry.rb +35 -3
  81. data/lib/roast/dsl/cog/stack.rb +1 -1
  82. data/lib/roast/dsl/cog/store.rb +5 -5
  83. data/lib/roast/dsl/cog.rb +70 -14
  84. data/lib/roast/dsl/cog_input_context.rb +36 -1
  85. data/lib/roast/dsl/cog_input_manager.rb +116 -7
  86. data/lib/roast/dsl/cogs/agent/config.rb +465 -0
  87. data/lib/roast/dsl/cogs/agent/input.rb +81 -0
  88. data/lib/roast/dsl/cogs/agent/output.rb +59 -0
  89. data/lib/roast/dsl/cogs/agent/provider.rb +51 -0
  90. data/lib/roast/dsl/cogs/agent/providers/claude/claude_invocation.rb +185 -0
  91. data/lib/roast/dsl/cogs/agent/providers/claude/message.rb +73 -0
  92. data/lib/roast/dsl/cogs/agent/providers/claude/messages/assistant_message.rb +36 -0
  93. data/lib/roast/dsl/cogs/agent/providers/claude/messages/result_message.rb +61 -0
  94. data/lib/roast/dsl/cogs/agent/providers/claude/messages/system_message.rb +47 -0
  95. data/lib/roast/dsl/cogs/agent/providers/claude/messages/text_message.rb +36 -0
  96. data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_result_message.rb +47 -0
  97. data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_use_message.rb +46 -0
  98. data/lib/roast/dsl/cogs/agent/providers/claude/messages/unknown_message.rb +27 -0
  99. data/lib/roast/dsl/cogs/agent/providers/claude/messages/user_message.rb +37 -0
  100. data/lib/roast/dsl/cogs/agent/providers/claude/tool_result.rb +51 -0
  101. data/lib/roast/dsl/cogs/agent/providers/claude/tool_use.rb +48 -0
  102. data/lib/roast/dsl/cogs/agent/providers/claude.rb +31 -0
  103. data/lib/roast/dsl/cogs/agent/stats.rb +92 -0
  104. data/lib/roast/dsl/cogs/agent/usage.rb +62 -0
  105. data/lib/roast/dsl/cogs/agent.rb +75 -0
  106. data/lib/roast/dsl/cogs/chat/config.rb +453 -0
  107. data/lib/roast/dsl/cogs/chat/input.rb +92 -0
  108. data/lib/roast/dsl/cogs/chat/output.rb +64 -0
  109. data/lib/roast/dsl/cogs/chat/session.rb +68 -0
  110. data/lib/roast/dsl/cogs/chat.rb +59 -56
  111. data/lib/roast/dsl/cogs/cmd.rb +251 -61
  112. data/lib/roast/dsl/cogs/ruby.rb +171 -0
  113. data/lib/roast/dsl/command_runner.rb +191 -0
  114. data/lib/roast/dsl/config_manager.rb +58 -11
  115. data/lib/roast/dsl/control_flow.rb +41 -0
  116. data/lib/roast/dsl/execution_manager.rb +162 -32
  117. data/lib/roast/dsl/nil_assertions.rb +23 -0
  118. data/lib/roast/dsl/system_cog/params.rb +32 -0
  119. data/lib/roast/dsl/system_cog.rb +36 -0
  120. data/lib/roast/dsl/system_cogs/call.rb +163 -0
  121. data/lib/roast/dsl/system_cogs/map.rb +454 -0
  122. data/lib/roast/dsl/system_cogs/repeat.rb +242 -0
  123. data/lib/roast/dsl/workflow.rb +26 -16
  124. data/lib/roast/dsl/workflow_context.rb +20 -0
  125. data/lib/roast/dsl/workflow_params.rb +24 -0
  126. data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
  127. data/lib/roast/sorbet_runtime_stub.rb +154 -0
  128. data/lib/roast/tools/apply_diff.rb +1 -3
  129. data/lib/roast/tools/cmd.rb +4 -3
  130. data/lib/roast/tools/read_file.rb +1 -1
  131. data/lib/roast/tools/update_files.rb +1 -1
  132. data/lib/roast/tools/write_file.rb +1 -1
  133. data/lib/roast/version.rb +1 -1
  134. data/lib/roast/workflow/base_workflow.rb +4 -0
  135. data/lib/roast/workflow/step_loader.rb +14 -2
  136. data/lib/roast-ai.rb +4 -0
  137. data/lib/roast.rb +58 -21
  138. data/{roast.gemspec → roast-ai.gemspec} +9 -13
  139. data/sorbet/rbi/gems/async@2.34.0.rbi +1577 -0
  140. data/sorbet/rbi/gems/cli-kit@5.2.0.rbi +2063 -0
  141. data/sorbet/rbi/gems/{cli-ui@2.3.0.rbi → cli-ui@2.7.0-6bdefd1d06305e5d6ae312ac76f9c88f88658dda.rbi} +1418 -1013
  142. data/sorbet/rbi/gems/console@1.34.2.rbi +1193 -0
  143. data/sorbet/rbi/gems/fiber-annotation@0.2.0.rbi +50 -0
  144. data/sorbet/rbi/gems/fiber-local@1.1.0.rbi +35 -0
  145. data/sorbet/rbi/gems/fiber-storage@1.0.1.rbi +41 -0
  146. data/sorbet/rbi/gems/io-event@1.14.0.rbi +724 -0
  147. data/sorbet/rbi/gems/metrics@0.15.0.rbi +9 -0
  148. data/sorbet/rbi/gems/traces@0.18.2.rbi +9 -0
  149. data/sorbet/rbi/shims/lib/roast/dsl/cog_input_context.rbi +1185 -5
  150. data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +311 -5
  151. data/sorbet/rbi/shims/lib/roast/dsl/execution_context.rbi +486 -5
  152. data/sorbet/tapioca/config.yml +6 -0
  153. data/sorbet/tapioca/require.rb +2 -0
  154. metadata +157 -30
  155. data/dsl/less_simple.rb +0 -112
  156. data/dsl/scoped_executors.rb +0 -28
  157. data/dsl/simple.rb +0 -8
  158. data/lib/roast/dsl/cogs/execute.rb +0 -46
  159. data/lib/roast/dsl/cogs/graph.rb +0 -53
  160. data/sorbet/rbi/gems/cgi@0.5.0.rbi +0 -2961
  161. data/sorbet/rbi/gems/claude_swarm@0.1.19.rbi +0 -568
  162. data/sorbet/rbi/gems/cli-kit@5.0.1.rbi +0 -1991
  163. data/sorbet/rbi/gems/dry-configurable@1.3.0.rbi +0 -672
  164. data/sorbet/rbi/gems/dry-core@1.1.0.rbi +0 -1894
  165. data/sorbet/rbi/gems/dry-inflector@1.2.0.rbi +0 -659
  166. data/sorbet/rbi/gems/dry-initializer@3.2.0.rbi +0 -781
  167. data/sorbet/rbi/gems/dry-logic@1.6.0.rbi +0 -1127
  168. data/sorbet/rbi/gems/dry-schema@1.14.1.rbi +0 -3727
  169. data/sorbet/rbi/gems/dry-types@1.8.3.rbi +0 -3969
  170. data/sorbet/rbi/gems/fast-mcp-annotations@1.5.3.rbi +0 -1588
  171. data/sorbet/rbi/gems/mime-types-data@3.2025.0617.rbi +0 -136
  172. data/sorbet/rbi/gems/mime-types@3.7.0.rbi +0 -1342
  173. data/sorbet/rbi/gems/rack@2.2.19.rbi +0 -5676
  174. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
  175. data/sorbet/rbi/gems/yard@0.9.37.rbi +0 -18492
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ config do
7
+ cmd { display! }
8
+ map(:words) do
9
+ # By default, maximum parallelism is 1 and map executions will run synchronously
10
+ # Calling `parallel` with a larger value will allow multiple map items to be run in parallel, up to the limit given
11
+ # Calling `parallel!` or `parallel(0)` will allow unlimited parallelism, processing all map items in parallel
12
+ parallel 3
13
+ end
14
+ end
15
+
16
+ execute(:capitalize_a_word) do
17
+ cmd(:to_upper) do |_, word, index|
18
+ sleep(0.1) if index == 3 # "three" will be slow --> finishing second last
19
+ sleep(0.2) if index == 1 # "one" will be slowest --> finishing last
20
+ ["sh", "-c", "echo \"#{word}\" | tr '[:lower:]' '[:upper:]'"]
21
+ end
22
+ end
23
+
24
+ execute do
25
+ # Call a subroutine with `map`
26
+ map(:words, run: :capitalize_a_word) do |my|
27
+ my.items = ["one", "two", "three", "four", "five", "six"]
28
+ my.initial_index = 1 # for convenience, just because our items begin with "one"
29
+ end
30
+
31
+ cmd do |my|
32
+ my.command = "echo"
33
+ # Regardless of the order in which the items were processed by a parallel map,
34
+ # their results will always be provided to `collect` and `reduce` in the order in which they were given.
35
+ my.args << collect(map!(:words)) { cmd!(:to_upper).text }.join(", ")
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ You just told me the following about a lake. Make some stuff up about why that's nonsense.
2
+
3
+ <%= lake_answer %>
data/dsl/prototype.rb CHANGED
@@ -4,12 +4,8 @@
4
4
  #: self as Roast::DSL::Workflow
5
5
 
6
6
  config do
7
- # TODO: make a way to do print_all on all and turn off printing on one
8
- cmd(:echo) { print_all! }
9
- cmd(:date) { print_stdout! }
10
- chat { "" }
11
- # TODO: add a way to apply config to a subset of named cog by pattern
12
- # cmd(/^date.*/) { print_all! }
7
+ cmd(:echo) { display! }
8
+ cmd(/date/) { display! }
13
9
  end
14
10
 
15
11
  execute do
@@ -21,5 +17,7 @@ execute do
21
17
  cmd(:echo) { |my| my.command = "echo 'Hello World!'" }
22
18
 
23
19
  # Cogs can implement input coercion for simple return values
24
- cmd(:date) { "date" }
20
+ fmt = "\"+%a %b %d %T %Z %Y\""
21
+ cmd(:date_today) { "/bin/date #{fmt}" }
22
+ cmd(:date_yesterday) { RUBY_PLATFORM.include?("darwin") ? "/bin/date -jv -1d #{fmt}" : "/bin/date -d '-1 day' #{fmt}" }
25
23
  end
@@ -0,0 +1,53 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ config do
7
+ end
8
+
9
+ execute do
10
+ repeat(:loop, run: :loop_body) { 7 }
11
+
12
+ ruby do
13
+ # You can access the final output value of a `repeat` cog directly
14
+ result = repeat!(:loop)
15
+ puts "Ultimate Loop Result: #{result.value}"
16
+ end
17
+
18
+ ruby do
19
+ puts "---"
20
+ # You can also access the final result, or individual cog results, of any specific iteration
21
+ # In the same manner as you would use `from` to access the results of a single `call` invocation.
22
+ puts "First Iteration Result: #{from(repeat!(:loop).first)}"
23
+ puts "Final Iteration Result: #{from(repeat!(:loop).last)}"
24
+ puts "Second-to-last Iteration Result: #{from(repeat!(:loop).iteration(-2))}"
25
+ # NOTE: accessing a specific iteration will raise an IndexException if the requested index is out of bounds
26
+ # for the number of iterations that ran.
27
+ end
28
+
29
+ ruby do
30
+ puts "---"
31
+ # You can access the results of all iterations in the same manner as you would use `collect` or `reduce`
32
+ # to access the complete results of a `map` invocation.
33
+ puts "All :add cog outputs: #{collect(repeat!(:loop).results) { ruby!(:add).value }}"
34
+ puts "Sum of :add cog output: #{reduce(repeat!(:loop).results, 0) { |acc| acc + ruby!(:add).value }}"
35
+ end
36
+ end
37
+
38
+ execute(:loop_body) do
39
+ # on each loop iteration, the input value provided will be the output value of the previous iteration
40
+ # the initial value will be what was provided when `repeat` was called in the outer scope.
41
+ ruby(:add) do |_, num, idx|
42
+ new_num = num + idx
43
+ s = "iteration #{idx}: #{num} + #{idx} -> #{new_num}"
44
+ puts s
45
+ new_num
46
+ end
47
+
48
+ ruby { |_, _, idx| break! if idx >= 3 }
49
+
50
+ # The value provided to `outputs` will be the input value for the next iteration
51
+ # On the final iteration, it will also be the `repeat` cog's own output value
52
+ outputs { ruby!(:add).value }
53
+ end
data/dsl/ruby_cog.rb ADDED
@@ -0,0 +1,72 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ class MyClass
7
+ class << self
8
+ def add_stuff(a, b)
9
+ a + b
10
+ end
11
+ end
12
+ end
13
+
14
+ config do
15
+ cmd { display! }
16
+ end
17
+
18
+ execute do
19
+ cmd(:roast) { "echo Roast" }
20
+
21
+ # The `ruby` cog just passes the return value from its input block directly to its output.
22
+ # Letting you write whatever ruby code you want to generate that output
23
+ ruby(:whatever) do
24
+ # Do whatever you want in this block
25
+ value = cmd!(:roast).text
26
+ puts "Hello, #{value.upcase}"
27
+ puts "Calling a method: #{MyClass.add_stuff(3, 4)}"
28
+ # The value you return will be exposed as the .value attribute on the cog's output
29
+ 1..5
30
+ end
31
+
32
+ cmd(:numbers) do |my|
33
+ my.command = "echo"
34
+ value = ruby!(:whatever).value.map { |n| n.to_s * n }
35
+ my.args = value
36
+ end
37
+
38
+ ruby(:advanced_hash_output) do
39
+ {
40
+ some_number: 7,
41
+ some_string: "Hello, world!",
42
+ multiply: proc { |a, b| a * b },
43
+ }
44
+ end
45
+
46
+ ruby do
47
+ result = ruby!(:advanced_hash_output) #: untyped
48
+
49
+ # If the output's `value` is a Hash, you can access its items directly from the output object
50
+ puts "Some Number + 1: #{result[:some_number] + 1}"
51
+ # You can also access its items as getter methods on the output object
52
+ puts "Some String to Upper: #{result.some_string.upcase}"
53
+ # And, if one of those items is a proc, you can call it directly on the output object
54
+ puts "Multiply 4 * 3: #{result.multiply(4, 3)}"
55
+ end
56
+
57
+ ruby(:advanced_object_output) do |my|
58
+ my.value = <<~STRING
59
+ This is a long block of test
60
+ Consisting of many lines
61
+ Three, to be precise
62
+ STRING
63
+ end
64
+
65
+ ruby do
66
+ result = ruby!(:advanced_object_output) #: untyped
67
+
68
+ # You can also call methods on the output's `value` directly, regardless of its type
69
+ puts "The long string has #{result.lines.length} lines"
70
+ puts "And it has #{result.length} characters"
71
+ end
72
+ end
@@ -0,0 +1,18 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ config do
7
+ agent do
8
+ provider :claude
9
+ model "haiku"
10
+ initial_prompt "Always respond in haiku form"
11
+ show_prompt!
12
+ dump_raw_agent_messages_to "tmp/claude-messages.log"
13
+ end
14
+ end
15
+
16
+ execute do
17
+ agent { "What is the world's largest lake?" }
18
+ end
data/dsl/simple_chat.rb CHANGED
@@ -4,9 +4,23 @@
4
4
  #: self as Roast::DSL::Workflow
5
5
 
6
6
  config do
7
- # ...
7
+ chat(:lake) { model("gpt-4o") }
8
+ chat(:next) { model("gpt-4o-mini") }
8
9
  end
9
10
 
10
11
  execute do
12
+ # Ask a question
11
13
  chat(:lake) { "What is the deepest lake?" }
14
+
15
+ # Continue the conversation (with a different model!)
16
+ chat(:next) do |my|
17
+ my.prompt = "What answer did you just give, and why?"
18
+ my.session = chat!(:lake).session
19
+ end
20
+
21
+ # Ask a question with a template prompt. You can pass variables to it as you would an ERB template
22
+ chat { template("dsl/prompts/simple_prompt.md.erb", { lake_answer: chat!(:lake).response }) }
23
+
24
+ # Shorthand to look up a template prompt
25
+ # chat { template("simple_prompt", { lake_answer: chat!(:lake).response }) }
12
26
  end
@@ -0,0 +1,29 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ config do
7
+ end
8
+
9
+ execute do
10
+ # You can use `repeat` to repeat an executor multiple times
11
+ # The executor will run forever until it breaks according to its internal logic
12
+ repeat(:loop, run: :loop_body) {}
13
+ end
14
+
15
+ execute(:loop_body) do
16
+ ruby(:one) do |_, _, idx|
17
+ s = "iteration #{idx}"
18
+ puts s
19
+ s
20
+ end
21
+
22
+ # Use `break!` to terminate the repeat loop
23
+ # Cogs that occur after `break!` is called will not run on the final iteration
24
+ ruby { |_, _, idx| break! if idx >= 3 }
25
+
26
+ # The `outputs` block will *always* run, including on the iteration in which the `break!` was issued
27
+ # NOTE: Be careful not to depend on the output of cogs that only run after the break point
28
+ outputs { |_, idx| "output of iteration #{idx}" }
29
+ end
data/dsl/skip.rb ADDED
@@ -0,0 +1,36 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ config do
7
+ cmd { display! }
8
+ end
9
+
10
+ execute do
11
+ cmd(:seconds) { "date +'%S'" }
12
+
13
+ cmd(:even) do
14
+ # `cmd!` output accessor bang method will raise an exception if the named cog did not complete successfully
15
+ seconds = cmd!(:seconds).out.to_i
16
+ # Use the `skip!` method in a cog's input proc to conditionally skip this cog
17
+ # If `skip!` is called, the input proc will immediately terminate
18
+ skip! if seconds.odd?
19
+ "echo '#{seconds} is even'"
20
+ end
21
+
22
+ cmd(:odd) do
23
+ seconds = cmd!(:seconds).out.to_i
24
+ skip! if seconds.even?
25
+ "echo '#{seconds} is odd'"
26
+ end
27
+
28
+ cmd do |my|
29
+ my.command = "echo"
30
+ # `cmd` output accessor non-bang method will return the cog's output or
31
+ # nil if the named cog did not run yet or did not complete successfully
32
+ my.args << "'even' cog ran" unless cmd(:even).nil?
33
+ # `cmd?` output accessor question method will return true/false if the cog did / did not complete successfully yet
34
+ my.args << "'odd' cog ran" if cmd?(:odd)
35
+ end
36
+ end
@@ -14,9 +14,8 @@ execute do
14
14
  cmd(:ls) { "ls -al" }
15
15
  cmd(:echo) do |my|
16
16
  my.command = "echo"
17
- # TODO: this is a bespoke output object for cmd, is there a generic one we can offer
18
- first_line = cmd(:ls).out.split("\n").second
19
- last_line = cmd(:ls).out.split("\n").last
17
+ first_line = cmd!(:ls).lines.second
18
+ last_line = cmd!(:ls).lines.last
20
19
  my.args << first_line unless first_line.blank?
21
20
  my.args << "\n---\n"
22
21
  my.args << last_line if last_line != first_line && last_line.present?
@@ -0,0 +1,57 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ config do
7
+ end
8
+
9
+ execute do
10
+ ruby do |_, params|
11
+ # Any files listed after the standard roast command-line args are collected as your workflows targets
12
+ # (shell globs are expanded as you would expect)
13
+ # e.g., `roast execute --executor=dsl dsl/targets_and_params.rb Gemfile Gemfile.lock`
14
+ # or, `roast execute --executor=dsl dsl/targets_and_params.rb Gemfile*`
15
+ puts "workflow targets: #{params.targets}"
16
+
17
+ # You can specify custom arguments for your workflow. Anything coming after `--` on the roast command
18
+ # line will be parsed as custom arguments.
19
+ # Simple word tokens are collected as `args`. Tokens in the form `key=value` are parsed into the `kwargs` hash.
20
+ # e.g., `roast execute --executor=dsl dsl/targets_and_params.rb Gemfile* -- foo=bar abc=pqr hello world`
21
+ puts "workflow args: #{params.args}" # [:hello, :world] (args are parsed as symbols)
22
+
23
+ # {abc: "pqr", foo: "bar"} (keys are parsed as symbols, values as strings)
24
+ # (Note: using explicit formatting for compatibility with Ruby versions < 3.4)
25
+ puts "workflow kwargs: {#{params.kwargs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")}}"
26
+ end
27
+
28
+ # There are convenience methods to access the workflow params from any cog input context in any scope
29
+ ruby do
30
+ puts
31
+ # `target!` will raise an exception unless exactly one target is provided
32
+ # puts "Explicit target = #{target!}"
33
+
34
+ # `targets` will return the (possibly empty) array of targets
35
+ puts "All targets = #{targets}"
36
+
37
+ # `arg?` will return a boolean indicating whether a specific value / flag argument is present
38
+ puts "Argument 'foo' provided? #{arg?(:foo) ? "yes" : "no"}"
39
+
40
+ # `args` will return the (possibly empty) array of simple value / flag arguments
41
+ puts "All args = #{args}"
42
+
43
+ # `kwarg` will return the value associated with a specific keyword argument,
44
+ # or nil if that keyword argument was not provided
45
+ puts "Keyword argument 'name': '#{kwarg(:name)}'"
46
+
47
+ # `kwarg!` will return the value associated with a specific keyword argument,
48
+ # or raise an exception if that keyword argument was not provided
49
+ # puts "Keyword argument 'name': #{kwarg!(:name)}"
50
+
51
+ # `kwarg?` will return a boolean indicating whether a specific keyword argument was provided
52
+ puts "Keyword argument 'name' provided: #{kwarg?(:name) ? "yes" : "no"}"
53
+
54
+ # `kwargs` will return the (possibly empty) hash of all keyword arguments
55
+ # puts "All kwargs = #{kwargs}"
56
+ end
57
+ end
@@ -0,0 +1,17 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ config do
5
+ chat(:low_temp) do
6
+ temperature(0.0)
7
+ end
8
+
9
+ chat(:high_temp) do
10
+ temperature(1.0)
11
+ end
12
+ end
13
+
14
+ execute do
15
+ chat(:low_temp) { "Write a haiku about the capital of France." }
16
+ chat(:high_temp) { "Write a haiku about the capital of France." }
17
+ end
@@ -0,0 +1,22 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ config do
7
+ end
8
+
9
+ execute do
10
+ ruby do
11
+ # The workflow automatically gets a temporary directory created for it
12
+ # unique to a single execution of the workflow, but shared across all executor scopes.
13
+ # This working directory will be cleaned up automatically when the workflow completes or fails.
14
+ puts tmpdir
15
+
16
+ # The value of `tmpdir` will always be a directory that exists
17
+ raise StandardError, "temporary directory does not exist" unless tmpdir.exist?
18
+
19
+ # The value of `tmpdir` will always be an absolute path
20
+ raise StandardError, "temporary directory is not an absolute path" unless tmpdir.absolute?
21
+ end
22
+ end
@@ -0,0 +1,179 @@
1
+ # Chapter 1: Your First Workflow
2
+
3
+ In this chapter, you'll create and run your first Roast workflow. We'll start with the absolute simplest example, then
4
+ show you how to add basic configuration.
5
+
6
+ ## What You'll Learn
7
+
8
+ - The basic structure of a Roast workflow file
9
+ - How to use the `chat` cog to interact with an LLM
10
+ - How to run workflows from the command line
11
+ - How to configure model settings
12
+
13
+ ## Understanding Workflows
14
+
15
+ Before we dive into code, let's understand what a Roast workflow is:
16
+
17
+ **A workflow is a sequence of steps that accomplish a task.** Think of it like a recipe: you define each step in order,
18
+ specify what inputs each step needs, and describe how they connect together.
19
+
20
+ ### Key Concepts
21
+
22
+ - **Steps** - Individual units of work (like "analyze this code" or "summarize this text")
23
+ - **Cogs** - The building blocks that implement step types (Roast comes with a comprehensive set of basic cogs for
24
+ interacting with LLMs, invoking coding agents, running shell commands, processing data, as wells as complex flow
25
+ control)
26
+ - **Declarative style** - You describe *what* you want done, the inputs each step needs, and how they connect together.
27
+ Roast takes care of the rest
28
+
29
+ For example, instead of writing code to open files, parse them, call APIs, handle errors, etc., you write:
30
+
31
+ ```ruby
32
+ execute do
33
+ chat { "Analyze this code and suggest improvements: #{my_code}" }
34
+ end
35
+ ```
36
+
37
+ Roast handles the details. You focus on the workflow.
38
+
39
+ In this chapter, we'll start with the simplest workflow: a single step using the `chat` cog. In later chapters, you'll
40
+ learn how to invoke coding agents, run shell commands, and chain multiple steps together to build sophisticated
41
+ workflows.
42
+
43
+ ## The Basics
44
+
45
+ Every Roast workflow is a Ruby file that defines what work to do. The simplest workflow has just one part:
46
+
47
+ 1. **Execute block** - Contains the work to perform
48
+
49
+ ### Minimal Syntax
50
+
51
+ ```ruby
52
+ execute do
53
+ # Your workflow goes here
54
+ end
55
+ ```
56
+
57
+ That's it! Everything happens inside the `execute do ... end` block.
58
+
59
+ ## Your First Chat Cog
60
+
61
+ The `chat` cog sends a prompt to a cloud-based LLM and gets a response back. Here's a simplest example:
62
+
63
+ ```ruby
64
+ execute do
65
+ chat { "Say hello!" }
66
+ end
67
+ ```
68
+
69
+ When you run this, Roast will:
70
+
71
+ 1. Send the prompt "Say hello!" to default LLM provider and model (OpenAI gpt-4o-mini)
72
+ 2. Wait for the response
73
+ 3. Display the response in your terminal, along with LLM usage statistics
74
+
75
+ ### Longer Prompts
76
+
77
+ For real workflows, you'll want more detailed prompts. Use Ruby's heredoc syntax for multi-line prompts:
78
+
79
+ ```ruby
80
+ execute do
81
+ chat do
82
+ <<~PROMPT
83
+ You are a helpful AI assistant.
84
+
85
+ Please introduce yourself and explain what Roast is in 2-3 sentences.
86
+ Keep your response friendly and concise.
87
+ PROMPT
88
+ end
89
+ end
90
+ ```
91
+
92
+ Or define your prompts in a template file using ERB syntax and include them in your workflow
93
+
94
+ ```ruby
95
+ execute do
96
+ chat { template("path_to_prompt_file.md.erb", { context: }) }
97
+ end
98
+ ```
99
+
100
+ ## Adding Configuration
101
+
102
+ You can configure which model to use, which provider, and other settings. Configuration goes in a `config do ... end`
103
+ block:
104
+
105
+ ```ruby
106
+ config do
107
+ chat do
108
+ model "gpt-4o-mini" # Use OpenAI's fast model
109
+ provider :openai # Use OpenAI (can also be :anthropic)
110
+ show_prompt! # Display the prompt before sending
111
+ end
112
+ end
113
+
114
+ execute do
115
+ chat do
116
+ <<~PROMPT
117
+ Explain what an AI workflow is in simple terms.
118
+ PROMPT
119
+ end
120
+ end
121
+ ```
122
+
123
+ ### Configuration Options
124
+
125
+ Common options you can set:
126
+
127
+ - `model "name"` - Which LLM model to use
128
+ - OpenAI: "gpt-4o", "gpt-4o-mini", "gpt-4-turbo"
129
+ - Anthropic: "claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"
130
+ - `provider :name` - Which LLM provider
131
+ - `:openai` or `:anthropic`
132
+ - `show_prompt!` - Display the prompt being sent
133
+ - `show_response!` - Display the response (on by default)
134
+ - `show_stats!` - Display token usage statistics (on by default)
135
+ - `no_display!` - Turn off all output (useful when you just want to pass the output to a subsequent cog)
136
+
137
+ ## Running the Workflows
138
+
139
+ To run any workflow in this chapter:
140
+
141
+ ```bash
142
+ # From the project root
143
+ bin/roast execute --executor=dsl dsl/tutorial/01_your_first_workflow/hello.rb
144
+ ```
145
+
146
+ Or the configured version:
147
+
148
+ ```bash
149
+ bin/roast execute --executor=dsl dsl/tutorial/01_your_first_workflow/configured_chat.rb
150
+ ```
151
+
152
+ You should see:
153
+
154
+ 1. The prompt being sent (if you enabled `show_prompt!`)
155
+ 2. The LLM's response
156
+ 3. Statistics about token usage
157
+
158
+ ## Try It Yourself
159
+
160
+ 1. **Run both example workflows** in this chapter
161
+ 2. **Modify the prompts** - Ask different questions
162
+ 3. **Try different models** - Change "gpt-4o-mini" to "gpt-4o" in the config
163
+ 4. **Experiment with display settings** - Try `no_show_stats!` and see what happens
164
+
165
+ ## Key Takeaways
166
+
167
+ - The `execute do ... end` block contains your workflow logic
168
+ - The `chat` cog sends prompts to cloud-based LLMs
169
+ - Use `<<~PROMPT ... PROMPT` for multi-line prompts
170
+ - The `config do ... end` block (before execute) sets up cog behavior
171
+ - Run workflows with `bin/roast execute --executor=dsl path/to/workflow.rb`
172
+
173
+ ## What's Next?
174
+
175
+ In the next chapter, you'll learn how to chain multiple cogs together, so the output of one becomes the input to
176
+ another. This is where Roast becomes powerful!
177
+
178
+ But first, make sure you can successfully run both workflows in this chapter. Experiment with different prompts and
179
+ configurations until you're comfortable.
@@ -0,0 +1,33 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ # This workflow demonstrates how to configure chat cogs.
7
+ # Configuration is set in a 'config' block before the 'execute' block.
8
+
9
+ config do
10
+ # Configure all chat cogs in this workflow
11
+ chat do
12
+ model "gpt-4o-mini" # Use OpenAI's fast, cost-effective model
13
+ provider :openai # Use OpenAI (alternative: :anthropic)
14
+ show_prompt! # Display the prompt before sending it
15
+ show_response! # Display the response (this is the default)
16
+ show_stats! # Display token usage statistics (default)
17
+ end
18
+ end
19
+
20
+ execute do
21
+ chat do
22
+ <<~PROMPT
23
+ You are a knowledgeable software development assistant.
24
+
25
+ Explain what a "workflow" is in the context of software automation,
26
+ using simple language that a beginner could understand.
27
+
28
+ Provide a brief example of when you might use a workflow.
29
+
30
+ Keep your response to 3-4 sentences.
31
+ PROMPT
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ # This type annotation helps Sorbet type-check your workflow.
5
+ # It's optional if you don't want to use Sorbet.
6
+ #: self as Roast::DSL::Workflow
7
+
8
+ # This is the simplest possible Roast workflow.
9
+ # It sends a single prompt to a chat LLM and displays the response.
10
+
11
+ config {}
12
+
13
+ execute do
14
+ chat do
15
+ <<~PROMPT
16
+ You are a friendly AI assistant helping someone learn Roast,
17
+ a Ruby-based workflow system for AI tasks.
18
+
19
+ Say hello and give them one encouraging tip about learning
20
+ new tools. Keep it brief and friendly!
21
+ PROMPT
22
+ end
23
+ end