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,185 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Agent < Cog
8
+ module Providers
9
+ class Claude < Provider
10
+ class ClaudeInvocation
11
+ class ClaudeInvocationError < Roast::Error; end
12
+
13
+ class ClaudeNotStartedError < ClaudeInvocationError; end
14
+
15
+ class ClaudeAlreadyStartedError < ClaudeInvocationError; end
16
+
17
+ class ClaudeNotCompletedError < ClaudeInvocationError; end
18
+
19
+ class ClaudeFailedError < ClaudeInvocationError; end
20
+
21
+ class Context
22
+ def initialize
23
+ @tool_uses = {} #: Hash[String, Messages::ToolUseMessage]
24
+ end
25
+
26
+ #: (String?) -> Messages::ToolUseMessage?
27
+ def tool_use(tool_use_id)
28
+ @tool_uses[tool_use_id] if tool_use_id
29
+ end
30
+
31
+ #: (Messages::ToolUseMessage) -> void
32
+ def add_tool_use(tool_use_message)
33
+ id = tool_use_message.id
34
+ @tool_uses[id] = tool_use_message if id
35
+ end
36
+ end
37
+
38
+ class Result
39
+ #: String
40
+ attr_accessor :response
41
+
42
+ #: bool
43
+ attr_accessor :success
44
+
45
+ #: String?
46
+ attr_accessor :session
47
+
48
+ #: Stats?
49
+ attr_accessor :stats
50
+
51
+ def initialize
52
+ @response = ""
53
+ @success = false
54
+ end
55
+ end
56
+
57
+ #: (Agent::Config, Agent::Input) -> void
58
+ def initialize(config, input)
59
+ @base_command = config.valid_command #: (String | Array[String])?
60
+ @model = config.valid_model #: String?
61
+ @append_system_prompt = config.valid_initial_prompt #: String?
62
+ @apply_permissions = config.apply_permissions? #: bool
63
+ @working_directory = config.valid_working_directory #: Pathname?
64
+ @prompt = input.valid_prompt! #: String
65
+ @session = input.session #: String?
66
+ @context = Context.new #: Context
67
+ @result = Result.new #: Result
68
+ @raw_dump_file = config.valid_dump_raw_agent_messages_to_path #: Pathname?
69
+ @show_progress = config.show_progress? #: bool
70
+ end
71
+
72
+ #: () -> void
73
+ def run!
74
+ raise ClaudeAlreadyStartedError if started?
75
+
76
+ @started = true
77
+ _stdout, stderr, status = CommandRunner.execute(
78
+ command_line,
79
+ working_directory: @working_directory,
80
+ stdin_content: @prompt,
81
+ stdout_handler: lambda { |line| handle_stdout(line) },
82
+ )
83
+
84
+ if status.success?
85
+ @completed = true
86
+ else
87
+ @failed = true
88
+ @result.success = false
89
+ @result.response += "\n" unless @result.response.blank? || @result.response.ends_with?("\n")
90
+ @result.response += stderr
91
+ end
92
+ end
93
+
94
+ #: () -> bool
95
+ def started?
96
+ @started ||= false
97
+ end
98
+
99
+ #: () -> bool
100
+ def running?
101
+ started? && !completed? && !failed?
102
+ end
103
+
104
+ #: () -> bool
105
+ def completed?
106
+ @completed ||= false
107
+ end
108
+
109
+ #: () -> bool
110
+ def failed?
111
+ @failed ||= false
112
+ end
113
+
114
+ #: () -> Result
115
+ def result
116
+ raise ClaudeNotStartedError unless started?
117
+ raise ClaudeFailedError, @result.response if failed?
118
+ raise ClaudeNotCompletedError, @result.response unless completed?
119
+
120
+ @result
121
+ end
122
+
123
+ private
124
+
125
+ #: (String) -> void
126
+ def handle_stdout(line)
127
+ line = line.strip
128
+ message = Message.from_json(line, raw_dump_file: @raw_dump_file) unless line.empty?
129
+ return unless message
130
+
131
+ handle_message(message)
132
+ end
133
+
134
+ #: (Message) -> void
135
+ def handle_message(message)
136
+ case message
137
+ when Messages::AssistantMessage
138
+ message.messages.each { |msg| handle_message(msg) }
139
+ when Messages::ResultMessage
140
+ @result.response = message.content
141
+ @result.success = message.success
142
+ @result.stats = message.stats
143
+ when Messages::ToolUseMessage
144
+ @context.add_tool_use(message)
145
+ when Messages::UserMessage
146
+ message.messages.each { |msg| handle_message(msg) }
147
+ end
148
+
149
+ @result.session = message.session_id if message.session_id.present?
150
+
151
+ formatted_message = message.format(@context)
152
+ puts formatted_message if formatted_message.present? && @show_progress
153
+
154
+ unless message.unparsed.blank?
155
+ # TODO: do something better with unhandled data so we can improve the parser
156
+ puts "[WARNING] Unhandled data in Claude #{message.type} message:"
157
+ puts JSON.pretty_generate(message.unparsed)
158
+ puts "[FULL MESSAGE: #{message.type}]"
159
+ pp(message)
160
+ end
161
+ end
162
+
163
+ #: () -> Array[String]
164
+ def command_line
165
+ command = if @base_command.is_a?(Array)
166
+ @base_command.dup
167
+ elsif @base_command.is_a?(String)
168
+ @base_command.split
169
+ else
170
+ ["claude"]
171
+ end
172
+ command.push("-p", "--verbose", "--output-format", "stream-json")
173
+ command.push("--model", @model) if @model
174
+ command.push("--append-system-prompt", @append_system_prompt) if @append_system_prompt
175
+ command.push("--fork-session", "--resume", @session) if @session.present?
176
+ command << "--dangerously-skip-permissions" unless @apply_permissions
177
+ command
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,73 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Agent < Cog
8
+ module Providers
9
+ class Claude < Provider
10
+ class Message
11
+ IGNORED_FIELDS = [
12
+ :uuid,
13
+ ].freeze
14
+
15
+ class << self
16
+ #: (String, ?raw_dump_file: Pathname?) -> Message
17
+ def from_json(json, raw_dump_file: nil)
18
+ raw_dump_file&.dirname&.mkpath
19
+ File.write("./tmp/claude-messages.log", "#{json}\n", mode: "a") if raw_dump_file
20
+ from_hash(JSON.parse(json, symbolize_names: true))
21
+ end
22
+
23
+ #: (Hash[Symbol, untyped]) -> Message
24
+ def from_hash(hash)
25
+ type = hash.delete(:type)&.to_sym
26
+ case type
27
+ when :assistant
28
+ Messages::AssistantMessage.new(type:, hash:)
29
+ when :result
30
+ Messages::ResultMessage.new(type:, hash:)
31
+ when :system
32
+ Messages::SystemMessage.new(type:, hash:)
33
+ when :text
34
+ Messages::TextMessage.new(type:, hash:)
35
+ when :tool_result
36
+ Messages::ToolResultMessage.new(type:, hash:)
37
+ when :tool_use
38
+ Messages::ToolUseMessage.new(type:, hash:)
39
+ when :user
40
+ Messages::UserMessage.new(type:, hash:)
41
+ else
42
+ Messages::UnknownMessage.new(type:, hash:)
43
+ end
44
+ end
45
+ end
46
+
47
+ #: String?
48
+ attr_reader :session_id
49
+
50
+ #: Symbol
51
+ attr_reader :type
52
+
53
+ #: Hash[Symbol, untyped]
54
+ attr_reader :unparsed
55
+
56
+ #: (type: Symbol, hash: Hash[Symbol, untyped]) -> void
57
+ def initialize(type:, hash:)
58
+ @session_id = hash.delete(:session_id)
59
+ @type = type
60
+ hash.except!(*IGNORED_FIELDS)
61
+ @unparsed = hash
62
+ end
63
+
64
+ #: (ClaudeInvocation::Context) -> String?
65
+ def format(context)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,36 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Agent < Cog
8
+ module Providers
9
+ class Claude < Provider
10
+ module Messages
11
+ class AssistantMessage < Message
12
+ IGNORED_FIELDS = [
13
+ :parent_tool_use_id,
14
+ ].freeze
15
+
16
+ #: Array[Message]
17
+ attr_reader :messages
18
+
19
+ #: (type: Symbol, hash: Hash[Symbol, untyped]) -> void
20
+ def initialize(type:, hash:)
21
+ @messages = hash.dig(:message, :content)&.map do |content|
22
+ content[:role] = :assistant
23
+ Message.from_hash(content)
24
+ end&.compact || []
25
+ hash.delete(:message)
26
+ hash.except!(*IGNORED_FIELDS)
27
+ super(type:, hash:)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,61 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Agent < Cog
8
+ module Providers
9
+ class Claude < Provider
10
+ module Messages
11
+ class ResultMessage < Message
12
+ IGNORED_FIELDS = [
13
+ :duration_api_ms,
14
+ :permission_denials,
15
+ :usage,
16
+ :uuid,
17
+ ].freeze
18
+
19
+ #: String
20
+ attr_reader :content
21
+
22
+ #: bool
23
+ attr_reader :success
24
+
25
+ #: Stats
26
+ attr_reader :stats
27
+
28
+ #: (type: Symbol, hash: Hash[Symbol, untyped]) -> void
29
+ def initialize(type:, hash:)
30
+ subtype = hash.delete(:subtype)
31
+ @content = hash.delete(:result) || ""
32
+ @success = hash.delete(:success) || subtype == "success"
33
+ if hash.delete(:is_error) || subtype == "error"
34
+ @content = @content || hash.dig(:error, :message) || "Unknown error"
35
+ hash.delete(:error)
36
+ end
37
+
38
+ @stats = Stats.new
39
+ @stats.duration_ms = hash.delete(:duration_ms)
40
+ @stats.num_turns = hash.delete(:num_turns)
41
+ hash.delete(:modelUsage)&.each do |model, h|
42
+ usage = Usage.new
43
+ usage.input_tokens = h[:inputTokens]
44
+ usage.output_tokens = h[:outputTokens]
45
+ usage.cost_usd = h[:costUSD]
46
+ @stats.model_usage[model] = usage
47
+ @stats.usage.input_tokens = (@stats.usage.input_tokens || 0) + (usage.input_tokens || 0)
48
+ @stats.usage.output_tokens = (@stats.usage.output_tokens || 0) + (usage.output_tokens || 0)
49
+ end
50
+ @stats.usage.cost_usd = hash.delete(:total_cost_usd)
51
+ hash.except!(*IGNORED_FIELDS)
52
+ super(type:, hash:)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,47 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Agent < Cog
8
+ module Providers
9
+ class Claude < Provider
10
+ module Messages
11
+ class SystemMessage < Message
12
+ IGNORED_FIELDS = [
13
+ :subtype,
14
+ :cwd,
15
+ :tools,
16
+ :mcp_servers,
17
+ :permissionMode,
18
+ :slash_commands,
19
+ :apiKeySource,
20
+ :claude_code_version,
21
+ :output_style,
22
+ :agents,
23
+ :skills,
24
+ :plugins,
25
+ ].freeze
26
+
27
+ #: String?
28
+ attr_reader :message
29
+
30
+ #: String?
31
+ attr_reader :model
32
+
33
+ #: (type: Symbol, hash: Hash[Symbol, untyped]) -> void
34
+ def initialize(type:, hash:)
35
+ @message = hash.delete(:message)
36
+ @model = hash.delete(:model)
37
+ hash.except!(*IGNORED_FIELDS)
38
+ super(type:, hash:)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,36 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Agent < Cog
8
+ module Providers
9
+ class Claude < Provider
10
+ module Messages
11
+ class TextMessage < Message
12
+ #: Symbol?
13
+ attr_reader :role
14
+
15
+ #: String
16
+ attr_reader :text
17
+
18
+ #: (type: Symbol, hash: Hash[Symbol, untyped]) -> void
19
+ def initialize(type:, hash:)
20
+ @role = hash.delete(:role)
21
+ @text = hash.delete(:text) || ""
22
+ super(type:, hash:)
23
+ end
24
+
25
+ #: (ClaudeInvocation::Context) -> String
26
+ def format(context)
27
+ @text
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,47 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Agent < Cog
8
+ module Providers
9
+ class Claude < Provider
10
+ module Messages
11
+ class ToolResultMessage < Message
12
+ IGNORED_FIELDS = [
13
+ :role,
14
+ ].freeze
15
+
16
+ #: String?
17
+ attr_reader :tool_use_id
18
+
19
+ #: String?
20
+ attr_reader :content
21
+
22
+ #: bool
23
+ attr_reader :is_error
24
+
25
+ #: (type: Symbol, hash: Hash[Symbol, untyped]) -> void
26
+ def initialize(type:, hash:)
27
+ @tool_use_id = hash.delete(:tool_use_id)
28
+ @content = hash.delete(:content)
29
+ @is_error = hash.delete(:is_error) || false
30
+ hash.except!(*IGNORED_FIELDS)
31
+ super(type:, hash:)
32
+ end
33
+
34
+ #: (ClaudeInvocation::Context) -> String?
35
+ def format(context)
36
+ tool_use = context.tool_use(tool_use_id)
37
+ tool_result = ToolResult.new(tool_use:, content:, is_error:)
38
+ tool_result.format
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Agent < Cog
8
+ module Providers
9
+ class Claude < Provider
10
+ module Messages
11
+ class ToolUseMessage < Message
12
+ IGNORED_FIELDS = [
13
+ :role,
14
+ ].freeze
15
+
16
+ #: String?
17
+ attr_reader :id
18
+
19
+ #: Symbol
20
+ attr_reader :name
21
+
22
+ #: Hash[Symbol, untyped]
23
+ attr_reader :input
24
+
25
+ #: (type: Symbol, hash: Hash[Symbol, untyped]) -> void
26
+ def initialize(type:, hash:)
27
+ @name = hash.delete(:name)&.downcase&.to_sym || :unknown
28
+ @id = hash.delete(:id)
29
+ @input = hash.delete(:input) || {}
30
+ hash.except!(*IGNORED_FIELDS)
31
+ super(type:, hash:)
32
+ end
33
+
34
+ #: (ClaudeInvocation::Context) -> String?
35
+ def format(context)
36
+ tool_use = ToolUse.new(name:, input:)
37
+ tool_use.format
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,27 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Agent < Cog
8
+ module Providers
9
+ class Claude < Provider
10
+ module Messages
11
+ class UnknownMessage < Message
12
+ #: Hash[Symbol, untyped]
13
+ attr_reader :raw
14
+
15
+ #: (type: Symbol, hash: Hash[Symbol, untyped]) -> void
16
+ def initialize(type:, hash:)
17
+ super(type:, hash:)
18
+ @raw = hash
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Agent < Cog
8
+ module Providers
9
+ class Claude < Provider
10
+ module Messages
11
+ class UserMessage < Message
12
+ IGNORED_FIELDS = [
13
+ :parent_tool_use_id,
14
+ :tool_use_result,
15
+ ].freeze
16
+
17
+ #: Array[Message]
18
+ attr_reader :messages
19
+
20
+ #: (type: Symbol, hash: Hash[Symbol, untyped]) -> void
21
+ def initialize(type:, hash:)
22
+ @messages = hash.dig(:message, :content)&.map do |content|
23
+ content[:role] = :user
24
+ Message.from_hash(content)
25
+ end&.compact || []
26
+ hash.delete(:message)
27
+ hash.except!(*IGNORED_FIELDS)
28
+ super(type:, hash:)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Agent < Cog
8
+ module Providers
9
+ class Claude < Provider
10
+ class ToolResult
11
+ #: Symbol?
12
+ attr_reader :tool_name
13
+
14
+ #: String?
15
+ attr_reader :tool_use_description
16
+
17
+ #: String?
18
+ attr_reader :content
19
+
20
+ #: bool
21
+ attr_reader :is_error
22
+
23
+ #: (tool_use: Messages::ToolUseMessage?, content: String?, is_error: bool) -> void
24
+ def initialize(tool_use:, content:, is_error:)
25
+ @tool_name = tool_use&.name || :unknown
26
+ @tool_use_description = tool_use&.input&.fetch(:description, nil) #: String?
27
+ @content = content
28
+ @is_error = is_error
29
+ end
30
+
31
+ #: () -> String
32
+ def format
33
+ format_method_name = "format_#{tool_name}".to_sym
34
+ return send(format_method_name) if respond_to?(format_method_name, true)
35
+
36
+ format_unknown
37
+ end
38
+
39
+ private
40
+
41
+ #: () -> String
42
+ def format_unknown
43
+ "UNKNOWN [#{tool_name}] #{is_error ? " ERROR" : "OK"} #{tool_use_description || ""}\n#{content}"
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,48 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Agent < Cog
8
+ module Providers
9
+ class Claude < Provider
10
+ class ToolUse
11
+ #: Symbol
12
+ attr_reader :name
13
+
14
+ #: Hash[Symbol, untyped]
15
+ attr_reader :input
16
+
17
+ #: (name: Symbol, input: Hash[Symbol, untyped]) -> void
18
+ def initialize(name:, input:)
19
+ @name = name
20
+ @input = input
21
+ end
22
+
23
+ #: () -> String
24
+ def format
25
+ format_method_name = "format_#{name}".to_sym
26
+ return send(format_method_name) if respond_to?(format_method_name, true)
27
+
28
+ format_unknown
29
+ end
30
+
31
+ private
32
+
33
+ #: () -> String
34
+ def format_bash
35
+ "BASH #{input.inspect}"
36
+ end
37
+
38
+ #: () -> String
39
+ def format_unknown
40
+ "UNKNOWN [#{name}] #{input.inspect}"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end