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,68 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ # This workflow demonstrates basic configuration of models and providers.
7
+ # It shows how to set global defaults and override them for specific steps.
8
+
9
+ config do
10
+ # Set default configuration for all chat cogs
11
+ chat do
12
+ model "gpt-4o-mini"
13
+ provider :openai
14
+ show_prompt! # Show prompts for all chat cogs
15
+ end
16
+
17
+ # Override for a specific cog - use more capable model
18
+ chat(:analyze_trends) do
19
+ model "gpt-5"
20
+ end
21
+
22
+ # Override for a specific cog - hide response
23
+ chat(:format_report) do
24
+ no_show_response!
25
+ end
26
+ end
27
+
28
+ execute do
29
+ # This cog uses the global config (gpt-4o-mini)
30
+ chat(:extract_info) do
31
+ <<~PROMPT
32
+ Extract the key information from this text and format it as a bullet list:
33
+
34
+ "Our Q4 results show revenue of $2.5M, up 15% from Q3. Customer count increased to 1,200
35
+ (from 950), and average deal size grew from $2,000 to $2,300. However, churn rate rose
36
+ slightly to 8% from 7%."
37
+ PROMPT
38
+ end
39
+
40
+ # This cog uses the specific override configured above (gpt-5)
41
+ chat(:analyze_trends) do
42
+ <<~PROMPT
43
+ Analyze these metrics and identify the most important trends:
44
+
45
+ #{chat!(:extract_info).text}
46
+
47
+ Focus on: growth patterns, concerning signals, and strategic implications.
48
+ PROMPT
49
+ end
50
+
51
+ # This cog uses the specific configuration above (hidden response)
52
+ chat(:format_report) do
53
+ <<~PROMPT
54
+ Format this analysis as a structured report with clear sections:
55
+
56
+ #{chat!(:analyze_trends).text}
57
+ PROMPT
58
+ end
59
+
60
+ # Final step: display the formatted report
61
+ ruby(:display) do
62
+ puts "\n" + "=" * 70
63
+ puts "QUARTERLY ANALYSIS REPORT"
64
+ puts "=" * 70
65
+ puts chat!(:format_report).response
66
+ puts "=" * 70 + "\n"
67
+ end
68
+ end
@@ -0,0 +1,156 @@
1
+ # Chapter 5: Control Flow
2
+
3
+ In previous chapters, you learned how to chain cogs together and configure them. Now you'll learn how to create
4
+ dynamic workflows that adapt based on conditions: skipping steps when needed, handling failures gracefully, and
5
+ checking whether steps actually ran.
6
+
7
+ ## What You'll Learn
8
+
9
+ - How to conditionally skip cogs with `skip!`
10
+ - How to handle cog failures with `fail!` and `no_abort_on_failure!`
11
+ - How command failures work and how to control them
12
+ - How to check if a cog ran successfully with three different accessors
13
+ - The difference between `!`, `?`, and non-bang accessors
14
+
15
+ ## Conditional Execution with skip!
16
+
17
+ Use `skip!` inside a cog's input block to conditionally skip that cog:
18
+
19
+ ```ruby
20
+ execute do
21
+ cmd(:check_status) { "curl https://api.example.com/status" }
22
+
23
+ cmd(:notify) do
24
+ status = cmd!(:check_status).out
25
+ skip! if status.include?("healthy")
26
+ "echo 'Service needs attention!'"
27
+ end
28
+ end
29
+ ```
30
+
31
+ When `skip!` is called, the cog immediately stops executing and is marked as skipped. Skipped cogs won't have
32
+ output and can be detected using the `?` accessor.
33
+
34
+ ## Checking if Cogs Ran
35
+
36
+ You have three ways to access cog outputs, each with different behavior:
37
+
38
+ - `cog!(:name)` - Returns output, raises error if cog didn't run yet, was skipped, or failed
39
+ - `cog?(:name)` - Returns `true` if cog ran and completed successfully, `false` otherwise
40
+ - `cog(:name)` - Returns output or `nil` if cog didn't run yet, was skipped, or failed
41
+
42
+ ```ruby
43
+ execute do
44
+ chat(:analyze) do
45
+ data = cmd(:fetch_data).out # Returns nil if fetch_data was skipped
46
+ skip! unless data
47
+ "Analyze this: #{data}"
48
+ end
49
+
50
+ ruby do
51
+ if chat?(:analyze)
52
+ puts "Analysis completed: #{chat!(:analyze).response}"
53
+ else
54
+ puts "Analysis was skipped"
55
+ end
56
+ end
57
+ end
58
+ ```
59
+
60
+ The `?` accessor is particularly useful for checking whether optional steps ran.
61
+
62
+ ## Handling Failures
63
+
64
+ Cogs can fail in two ways: by explicitly calling `fail!`, or by encountering errors during execution (like a command
65
+ returning a non-zero exit code). By default, any cog failure will also abort the entire workflow.
66
+
67
+ ### Explicit Failure with fail!
68
+
69
+ Use `fail!` to terminate a cog when conditions prevent successful execution:
70
+
71
+ ```ruby
72
+ execute do
73
+ agent(:process) do |my|
74
+ file = Pathname.new("my/data/file.json")
75
+ fail! unless file.exist?
76
+ my.prompt = "Process this file: #{file.realpath}"
77
+ end
78
+ end
79
+ ```
80
+
81
+ ### Command Failures
82
+
83
+ By default, the `cmd` cog automatically fails when a command returns a non-zero exit status.
84
+ And also by default, when a cog fails the entire workflow is aborted.
85
+ You can control both aspects of this behavior:
86
+
87
+ ```ruby
88
+ config do
89
+ cmd(:risky) do
90
+ # This command might fail, but continue the workflow anyway if it does
91
+ no_abort_on_failure!
92
+ end
93
+ cmd(:grep) do
94
+ # This command might exit with a non-zero status code, but that doesn't represent a failure for it
95
+ no_fail_on_error!
96
+ end
97
+ end
98
+
99
+ execute do
100
+ cmd(:risky) { "[ $RANDOM -gt 30000 ] && echo 'it worked!'" } # This command will probably fail
101
+
102
+ # We expect a non-zero exit code here as part of normal operations.
103
+ # `grep` matching no lines does not represent a failure condition for our workflow.
104
+ cmd(:grep) { "grep 'pattern' file.txt" }
105
+
106
+ ruby do
107
+ puts cmd?(:risky) ? "Risky command succeeded" : "Risky command failed"
108
+ puts "Grep matched #{cmd(:grep).lines.length} lines"
109
+ end
110
+ end
111
+ ```
112
+
113
+ Use `no_abort_on_failure!` to let the workflow continue even when a cog fails. The non-bang and `?` accessors let you
114
+ check the result and handle failures gracefully.
115
+
116
+ Use `no_fail_on_error!` with the `cmd` cog specifically to indicate that a non-zero status code should not
117
+ be considered a failure.
118
+
119
+ ## Running the Workflows
120
+
121
+ To run the examples in this chapter:
122
+
123
+ ```bash
124
+ # Conditional execution example
125
+ bin/roast execute --executor=dsl dsl/tutorial/05_control_flow/conditional_execution.rb
126
+ ```
127
+
128
+ ```bash
129
+ # Handling failures example
130
+ bin/roast execute --executor=dsl dsl/tutorial/05_control_flow/handling_failures.rb
131
+ ```
132
+
133
+ ## Try It Yourself
134
+
135
+ 1. **Add conditions** - Use `skip!` to create optional workflow steps
136
+ 2. **Check outcomes** - Use the `?` accessor to branch based on whether steps ran
137
+ 3. **Handle failures** - Use `fail!` for validation and the non-bang accessor for recovery
138
+ 4. **Combine approaches** - Mix conditional execution with the techniques from previous chapters
139
+
140
+ ## Key Takeaways
141
+
142
+ - Use `skip!` to conditionally skip cogs based on runtime conditions
143
+ - Use `fail!` to explicitly terminate cogs that can't complete successfully
144
+ - Commands automatically fail on non-zero exit status (configurable with `no_fail_on_error!`)
145
+ - Use `no_abort_on_failure!` to continue workflows even when cogs fail
146
+ - Use `cog?(:name)` to check if a cog ran successfully
147
+ - Use `cog(:name)` (non-bang) to safely access outputs that might not exist
148
+ - Use `cog!(:name)` when you expect the cog to have run successfully
149
+ - Control flow makes workflows dynamic and adaptive
150
+
151
+ ## What's Next?
152
+
153
+ In the next chapter, you'll learn about processing collections: using `map` to apply operations across multiple items
154
+ and building workflows that handle batches of data.
155
+
156
+ But first, experiment with control flow to create workflows that adapt to different conditions!
@@ -0,0 +1,62 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ # This workflow demonstrates conditional execution using skip! and the ? accessor.
7
+ # It checks a random number and runs different steps based on whether it's even or odd.
8
+
9
+ config do
10
+ cmd do
11
+ display!
12
+ end
13
+ end
14
+
15
+ execute do
16
+ # Generate a random number
17
+ cmd(:random_number) { "echo $RANDOM" }
18
+
19
+ # This step only runs if the number is even
20
+ cmd(:process_even) do
21
+ number = cmd!(:random_number).text.to_i
22
+ skip! if number.odd?
23
+ "echo 'Processing even number: #{number}'"
24
+ end
25
+
26
+ # This step only runs if the number is odd
27
+ cmd(:process_odd) do
28
+ number = cmd!(:random_number).text.to_i
29
+ skip! if number.even?
30
+ "echo 'Processing odd number: #{number}'"
31
+ end
32
+
33
+ # This step always runs and reports which path was taken
34
+ cmd do |my|
35
+ my.command = "echo"
36
+
37
+ # Use the ? accessor to check which steps ran
38
+ my.args << if cmd?(:process_even)
39
+ "Even path executed"
40
+ elsif cmd?(:process_odd)
41
+ "Odd path executed"
42
+ else
43
+ "Neither path executed (unexpected!)"
44
+ end
45
+ end
46
+
47
+ # Demonstrate using non-bang accessor
48
+ ruby do
49
+ puts "\n" + "=" * 70
50
+ puts "CONDITIONAL EXECUTION RESULTS"
51
+ puts "=" * 70
52
+
53
+ number = cmd!(:random_number).out.to_i
54
+ puts "Random number was: #{number}"
55
+
56
+ # Non-bang accessor returns nil if cog didn't run
57
+ puts "Even result: #{cmd(:process_even) || "n/a"}"
58
+ puts "Odd result: #{cmd(:process_odd) || "n/a"}"
59
+
60
+ puts "=" * 70 + "\n"
61
+ end
62
+ end
@@ -0,0 +1,77 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ # This workflow demonstrates failure handling with both no_abort_on_failure! and no_fail_on_error!
7
+ # It shows how workflows can continue even when individual cogs fail.
8
+
9
+ config do
10
+ # This chat cog might fail, but the workflow should continue
11
+ chat(:followup) do
12
+ no_abort_on_failure!
13
+ no_display!
14
+ end
15
+
16
+ # This command might return non-zero, but that's not a failure
17
+ cmd(:grep) do
18
+ no_fail_on_error!
19
+ no_display!
20
+ end
21
+ end
22
+
23
+ execute do
24
+ # Step 1: Ask LLM to make up some data and return it as JSON
25
+ chat(:generate_data) do
26
+ <<~PROMPT
27
+ Make up a simple shopping list with 3-5 items as a JSON object
28
+ `{ items: [ name: ..., quantity: ... ] }`.
29
+ PROMPT
30
+ end
31
+
32
+ # Step 2: Ask a follow-up question, but fail if the original response didn't include "milk"
33
+ chat(:followup) do
34
+ shopping_list_items = chat!(:generate_data).json![:items].map { |it| it[:name].downcase }
35
+ fail! unless shopping_list_items.include?("milk")
36
+
37
+ <<~PROMPT
38
+ Based on this shopping list:
39
+ #{chat!(:generate_data).text}
40
+
41
+ Suggest a recipe that uses milk from the list.
42
+ PROMPT
43
+ end
44
+
45
+ # Step 3: Run a command that might fail (grep returns non-zero when no matches)
46
+ cmd(:grep) do |my|
47
+ my.command = "grep -i eggs"
48
+ my.stdin = chat!(:generate_data).text
49
+ end
50
+
51
+ # Step 4: Print summary results
52
+ ruby do
53
+ puts "\n" + "=" * 70
54
+ puts "WORKFLOW RESULTS"
55
+ puts "=" * 70
56
+
57
+ shopping_list = chat!(:generate_data).json!
58
+ puts "\nGenerated shopping list:"
59
+ shopping_list[:items].each { |it| puts "- #{it[:name]}: #{it[:quantity]}" }
60
+
61
+ if chat?(:followup)
62
+ puts "\n✓ Follow-up succeeded (milk was in the list):"
63
+ puts chat!(:followup).text
64
+ else
65
+ puts "\n✗ Follow-up failed (milk was not in the list)"
66
+ end
67
+
68
+ if cmd!(:grep).status.exitstatus == 0
69
+ puts "\n✓ Grep found matches:"
70
+ puts cmd!(:grep).text
71
+ else
72
+ puts "\n✗ Grep found no matches"
73
+ end
74
+
75
+ puts "\n" + "=" * 70
76
+ end
77
+ end
@@ -0,0 +1,172 @@
1
+ # Chapter 6: Reusable Scopes
2
+
3
+ In previous chapters, all your workflows used a single `execute` block. Now you'll learn how to create reusable scopes
4
+ that can be called multiple times with different inputs, making your workflows more modular and maintainable.
5
+
6
+ ## What You'll Learn
7
+
8
+ - How to define named execute scopes
9
+ - How to call scopes with the `call` cog
10
+ - How to pass values to scopes
11
+ - How to return values from scopes with `outputs!`
12
+ - How to extract outputs using `from()`
13
+
14
+ ## Named Execute Scopes
15
+
16
+ Define a named execute scope by providing a name to `execute`. Just like the top-level execute scope, you can define
17
+ however many steps you want, and they'll run in order, top-to-bottom. Cogs can access the outputs of previous
18
+ cog's in their own scope, but cannot access the output of cogs defined in other scopes.
19
+
20
+ ```ruby
21
+ execute(:process_file) do
22
+ cmd(:word_count) { "wc -w #{filename}" }
23
+ ruby { puts "Words: #{cmd!(:word_count).text}" }
24
+ end
25
+ ```
26
+ You define all execute scopes at the top level of your workflow file. But, you can define them in any order.
27
+ Named scopes don't run automatically. They must be called explicitly using a cog like the `call` cog.
28
+
29
+ ## The call Cog
30
+
31
+ Use the `call` cog to invoke a named scope:
32
+
33
+ ```ruby
34
+ execute(:greet) do
35
+ chat { "say 'Hello, World!'" }
36
+ chat { "Tell me a funny joke" }
37
+ end
38
+
39
+ execute do
40
+ call(run: :greet) # Invokes the :greet scope
41
+ end
42
+ ```
43
+
44
+ You can call the same scope multiple times:
45
+
46
+ ```ruby
47
+ execute do
48
+ call(run: :greet)
49
+ call(run: :greet)
50
+ call(run: :greet)
51
+ end
52
+ ```
53
+
54
+ ## Passing Values to Scopes
55
+
56
+ Pass values to a called scope, and access them as the second block parameter in any cog's input block:
57
+
58
+ ```ruby
59
+ execute(:greet_person) do
60
+ chat do |my, name|
61
+ my.prompt = "Say hello to #{name}!"
62
+ end
63
+ end
64
+
65
+ execute do
66
+ call(run: :greet_person) { "Alice" }
67
+ call(run: :greet_person) { "Bob" }
68
+ end
69
+ ```
70
+
71
+ The value from the `call` cog becomes available as the second parameter to any cog in the called scope.
72
+
73
+ ## Returning Values with outputs!
74
+
75
+ Use `outputs!` to specify what a scope returns:
76
+
77
+ ```ruby
78
+ execute(:double_number) do
79
+ ruby(:calculate) { |_, number| number * 2 }
80
+
81
+ outputs! { ruby!(:calculate).value }
82
+ end
83
+
84
+ execute do
85
+ call(:result, run: :double_number) { 21 }
86
+
87
+ ruby do
88
+ answer = from(call!(:result))
89
+ puts "The answer is: #{answer}" # => 42
90
+ end
91
+ end
92
+ ```
93
+
94
+ The `from()` helper extracts the final output from a called scope.
95
+
96
+ ## Extracting output values from specific cogs
97
+
98
+ The `outputs!` block is optional. In its absence, the return value will be the output value of the final cog
99
+ in the scope. You can also pass a block to `from` and access the output of any cog(s) you want from the scope.
100
+
101
+ ```ruby
102
+ execute(:number_math) do
103
+ ruby(:add_two) { |_, number| number + 2 }
104
+ ruby(:subtract_two) { |_, number| number - 2 }
105
+ ruby(:multiply_by_two) { |_, number| number * 2 }
106
+ ruby(:divide_by_two) { |_, number| number / 2 }
107
+ end
108
+
109
+ execute do
110
+ # the return value of the block given to `call` will be the value passed to the scope being run,
111
+ # but you can also set the value explicitly
112
+ call(:result, run: :number_math) { |my| my.value = 28 }
113
+
114
+ ruby do
115
+ answer = from(call!(:result)).value # this yields the output of the final cog in :number_math: ruby!(:divide_by_two)
116
+ puts "The final answer is: #{answer}" # => 14
117
+
118
+ # pass a block to `from` to access the output of other cogs from the scope that was run
119
+ # this block runs in the context of the other scope and can access any cogs defined in it
120
+ subtraction, multiplication = from(call!(:result)) do
121
+ [
122
+ ruby!(:subtract_two).value,
123
+ ruby!(:multiply_by_two).value
124
+ ]
125
+ end
126
+ puts "Some intermediate answers are: #{subtraction} (subtraction) and #{multiplication} (multiplication)"
127
+ end
128
+ end
129
+ ```
130
+
131
+ ## Running the Workflows
132
+
133
+ To run the examples in this chapter:
134
+
135
+ ```bash
136
+ # Basic scope example
137
+ bin/roast execute --executor=dsl dsl/tutorial/06_reusable_scopes/basic_scope.rb
138
+ ```
139
+
140
+ ```bash
141
+ # Parameterized scope example
142
+ bin/roast execute --executor=dsl dsl/tutorial/06_reusable_scopes/parameterized_scope.rb
143
+ ```
144
+
145
+ ```bash
146
+ # Accessing specific scope outputs
147
+ bin/roast execute --executor=dsl dsl/tutorial/06_reusable_scopes/accessing_scope_outputs.rb
148
+ ```
149
+
150
+ ## Try It Yourself
151
+
152
+ 1. **Create reusable logic** - Extract common operations into named scopes
153
+ 2. **Parameterize scopes** - Pass different values to the same scope
154
+ 3. **Return values** - Use `outputs!` to capture 'default' result values from scopes
155
+ 4. **Chain scopes** - Call scopes from within other scopes
156
+
157
+ ## Key Takeaways
158
+
159
+ - Use `execute(:name) do ... end` to define reusable scopes
160
+ - Use `call(run: :scope_name)` to invoke named scopes
161
+ - Pass values to scopes using the block parameter of `call`
162
+ - Access passed values as the second parameter in cog input blocks
163
+ - Use `outputs!` to specify what a scope returns
164
+ - Use `from(call!(:name))` to extract the return value
165
+ - Named scopes don't run unless explicitly called
166
+
167
+ ## What's Next?
168
+
169
+ In the next chapter, you'll learn about `map`—a powerful way to apply a scope to every item in a collection, enabling
170
+ batch processing and parallel execution.
171
+
172
+ But first, experiment with reusable scopes to make your workflows more modular!
@@ -0,0 +1,126 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ #: self as Roast::DSL::Workflow
5
+
6
+ # This workflow demonstrates accessing specific cog outputs from a called scope.
7
+ # It shows how to use from() with a block to extract outputs from multiple cogs,
8
+ # and how the default return value is the final cog's output when no outputs! block is provided.
9
+
10
+ config do
11
+ chat do
12
+ model "gpt-4o-mini"
13
+ no_show_response!
14
+ end
15
+
16
+ chat(:final_summary) do
17
+ show_response!
18
+ end
19
+ end
20
+
21
+ # Define a scope that processes text through multiple transformations
22
+ # Note: No outputs! block, so the default return is the last cog's output
23
+ execute(:process_text) do
24
+ chat(:extract_keywords) do |_, text|
25
+ <<~PROMPT
26
+ Extract 3-5 key words or phrases from this text, in JSON form: `{ keywords: [ ... ] }`
27
+ #{text}
28
+ PROMPT
29
+ end
30
+
31
+ chat(:determine_sentiment) do |_, text|
32
+ <<~PROMPT
33
+ What is the sentiment of this text? Answer with just one word: positive, negative, or neutral.
34
+ #{text}
35
+ PROMPT
36
+ end
37
+
38
+ chat(:generate_title) do |_, text|
39
+ <<~PROMPT
40
+ Generate a short, catchy title (5-7 words) for this text:
41
+ #{text}
42
+ PROMPT
43
+ end
44
+
45
+ chat(:word_count) do |_, text|
46
+ "Count the approximate number of words in this text and respond with just the number: #{text}"
47
+ end
48
+ end
49
+
50
+ # Main workflow
51
+ execute do
52
+ ruby do
53
+ puts "=" * 70
54
+ puts "ACCESSING SCOPE OUTPUTS EXAMPLE"
55
+ puts "=" * 70
56
+ end
57
+
58
+ # Process a sample text
59
+ call(:result, run: :process_text) do
60
+ <<~TEXT
61
+ Artificial intelligence is revolutionizing software development.
62
+ Machine learning models can now write code, detect bugs, and
63
+ suggest improvements. This technology empowers developers to
64
+ build better software faster than ever before.
65
+ TEXT
66
+ end
67
+
68
+ # The default return value (without outputs!) is the last cog's output
69
+ ruby do
70
+ puts "\nDEFAULT RETURN VALUE (last cog in scope):"
71
+ word_count = from(call!(:result)).text
72
+ puts "Word count: #{word_count}"
73
+ end
74
+
75
+ # Use from() with a block to access specific cogs from the scope
76
+ ruby do
77
+ puts "\nACCESSING SPECIFIC COGS:"
78
+
79
+ # The block runs in the context of the called scope
80
+ # You can access any cogs defined in that scope
81
+ keywords, sentiment, title = from(call!(:result)) do
82
+ [
83
+ chat!(:extract_keywords).json![:keywords],
84
+ chat!(:determine_sentiment).text,
85
+ chat!(:generate_title).text,
86
+ ]
87
+ end
88
+
89
+ puts "Keywords: #{keywords.join(", ")}"
90
+ puts "Sentiment: #{sentiment}"
91
+ puts "Title: #{title}"
92
+ end
93
+
94
+ # Create a final summary combining the extracted information
95
+ chat(:final_summary) do
96
+ keywords, sentiment, title, word_count = from(call!(:result)) do
97
+ [
98
+ chat!(:extract_keywords).json![:keywords],
99
+ chat!(:determine_sentiment).text,
100
+ chat!(:generate_title).text,
101
+ chat!(:word_count).text,
102
+ ]
103
+ end
104
+
105
+ <<~PROMPT
106
+ Create a brief meta-summary (2-3 sentences) using this analysis:
107
+
108
+ Title: #{title}
109
+ Keywords: #{keywords.join(", ")}
110
+ Sentiment: #{sentiment}
111
+ Word Count: #{word_count}
112
+ PROMPT
113
+ end
114
+
115
+ ruby do
116
+ puts "\n" + "=" * 70
117
+ puts <<~NOTE
118
+ KEY POINTS:
119
+ - Without outputs!, the default return is the last cog's output
120
+ - Use from(call!(:name)) to access the default return value
121
+ - Use from(call!(:name)) { ... } with a block to access specific cogs
122
+ - The block runs in the context of the called scope
123
+ - You can extract outputs from multiple cogs in one call
124
+ NOTE
125
+ end
126
+ end