roast-ai 1.0.2 → 1.2.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.
- checksums.yaml +4 -4
- data/.claude/commands/docs/write-comments.md +1 -1
- data/.rubocop.yml +12 -1
- data/Gemfile +2 -2
- data/Gemfile.lock +149 -34
- data/README.md +56 -3
- data/examples/agent_with_multiple_prompts.rb +27 -0
- data/examples/custom_logging.rb +4 -2
- data/examples/demo/Gemfile.lock +49 -15
- data/examples/plugin-gem-example/Gemfile.lock +19 -15
- data/examples/simple_chat.rb +1 -1
- data/examples/simple_pi_agent.rb +18 -0
- data/internal/rubocop/cop/roast/no_test_class_nesting.rb +126 -0
- data/internal/rubocop/rubocop-roast.yml +6 -0
- data/internal/workflows/maintenance/branch_docs_impact.rb +97 -0
- data/internal/workflows/maintenance/deprecated_models_docs_updater.rb +78 -0
- data/lib/roast/cog/config.rb +1 -1
- data/lib/roast/cog/output.rb +2 -1
- data/lib/roast/cog/registry.rb +3 -3
- data/lib/roast/cog_input_manager.rb +28 -7
- data/lib/roast/cogs/agent/config.rb +2 -2
- data/lib/roast/cogs/agent/input.rb +20 -22
- data/lib/roast/cogs/agent/providers/claude/claude_invocation.rb +13 -5
- data/lib/roast/cogs/agent/providers/claude/messages/result_message.rb +1 -1
- data/lib/roast/cogs/agent/providers/claude/tool_result.rb +344 -4
- data/lib/roast/cogs/agent/providers/claude/tool_use.rb +356 -1
- data/lib/roast/cogs/agent/providers/claude.rb +16 -3
- data/lib/roast/cogs/agent/providers/pi/messages/tool_call_message.rb +60 -0
- data/lib/roast/cogs/agent/providers/pi/messages/tool_result_message.rb +57 -0
- data/lib/roast/cogs/agent/providers/pi/pi_invocation.rb +352 -0
- data/lib/roast/cogs/agent/providers/pi.rb +41 -0
- data/lib/roast/cogs/agent/stats.rb +29 -0
- data/lib/roast/cogs/agent/usage.rb +22 -0
- data/lib/roast/cogs/agent.rb +5 -6
- data/lib/roast/cogs/chat/config.rb +28 -2
- data/lib/roast/cogs/chat.rb +82 -10
- data/lib/roast/event.rb +1 -0
- data/lib/roast/event_monitor.rb +35 -3
- data/lib/roast/log.rb +21 -0
- data/lib/roast/log_formatter.rb +9 -7
- data/lib/roast/version.rb +1 -1
- data/lib/roast.rb +1 -3
- data/roast-ai.gemspec +2 -1
- data/sorbet/rbi/gems/activesupport@8.0.2.rbi +549 -383
- data/sorbet/rbi/gems/addressable@2.8.7.rbi +46 -44
- data/sorbet/rbi/gems/ast@2.4.3.rbi +7 -6
- data/sorbet/rbi/gems/async@2.34.0.rbi +21 -3
- data/sorbet/rbi/gems/benchmark@0.4.1.rbi +7 -7
- data/sorbet/rbi/gems/bigdecimal@3.2.2.rbi +198 -1
- data/sorbet/rbi/gems/concurrent-ruby@1.3.5.rbi +405 -328
- data/sorbet/rbi/gems/console@1.34.2.rbi +2 -2
- data/sorbet/rbi/gems/docile@1.4.1.rbi +30 -30
- data/sorbet/rbi/gems/drb@2.2.3.rbi +25 -25
- data/sorbet/rbi/gems/erubi@1.13.1.rbi +2 -0
- data/sorbet/rbi/gems/faraday-net_http@3.4.2.rbi +2 -77
- data/sorbet/rbi/gems/faraday-retry@2.3.2.rbi +2 -57
- data/sorbet/rbi/gems/faraday@2.14.1.rbi +382 -75
- data/sorbet/rbi/gems/guard-compat@1.2.1.rbi +1 -110
- data/sorbet/rbi/gems/guard-minitest@2.4.6.rbi +0 -139
- data/sorbet/rbi/gems/guard@2.19.1.rbi +38 -38
- data/sorbet/rbi/gems/hashdiff@1.2.0.rbi +3 -3
- data/sorbet/rbi/gems/i18n@1.14.7.rbi +53 -29
- data/sorbet/rbi/gems/io-event@1.14.0.rbi +67 -10
- data/sorbet/rbi/gems/json@2.18.1.rbi +227 -5
- data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +83 -0
- data/sorbet/rbi/gems/listen@3.9.0.rbi +7 -7
- data/sorbet/rbi/gems/logger@1.7.0.rbi +3 -3
- data/sorbet/rbi/gems/lumberjack@1.2.10.rbi +21 -21
- data/sorbet/rbi/gems/marcel@1.1.0.rbi +1 -1
- data/sorbet/rbi/gems/minitest-rg@5.3.0.rbi +0 -96
- data/sorbet/rbi/gems/minitest@5.25.5.rbi +1 -16
- data/sorbet/rbi/gems/net-http@0.9.1.rbi +27 -19
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +18 -0
- data/sorbet/rbi/gems/notiffany@0.1.3.rbi +20 -20
- data/sorbet/rbi/gems/ostruct@0.6.2.rbi +149 -15
- data/sorbet/rbi/gems/parser@3.3.8.0.rbi +141 -139
- data/sorbet/rbi/gems/prism@1.4.0.rbi +922 -864
- data/sorbet/rbi/gems/public_suffix@6.0.2.rbi +56 -35
- data/sorbet/rbi/gems/racc@1.8.1.rbi +10 -2
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +12 -12
- data/sorbet/rbi/gems/rake@13.3.0.rbi +219 -318
- data/sorbet/rbi/gems/{rbi@0.3.6.rbi → rbi@0.3.9.rbi} +612 -2267
- data/sorbet/rbi/gems/{rbs@3.9.4.rbi → rbs@4.0.0.dev.5.rbi} +2013 -680
- data/sorbet/rbi/gems/regexp_parser@2.10.0.rbi +151 -113
- data/sorbet/rbi/gems/require-hooks@0.2.3.rbi +110 -0
- data/sorbet/rbi/gems/rexml@3.4.2.rbi +24 -51
- data/sorbet/rbi/gems/rubocop-ast@1.45.1.rbi +506 -815
- data/sorbet/rbi/gems/rubocop-sorbet@0.10.5.rbi +16 -16
- data/sorbet/rbi/gems/rubocop@1.77.0.rbi +2692 -2327
- data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +8 -8
- data/sorbet/rbi/gems/ruby_llm@1.8.2.rbi +38 -23
- data/sorbet/rbi/gems/securerandom@0.4.1.rbi +1 -1
- data/sorbet/rbi/gems/simplecov-html@0.13.2.rbi +2 -131
- data/sorbet/rbi/gems/simplecov@0.22.0.rbi +28 -127
- data/sorbet/rbi/gems/{spoom@1.6.3.rbi → spoom@1.7.11.rbi} +1139 -2246
- data/sorbet/rbi/gems/sqlite3@2.9.0.rbi +91 -1
- data/sorbet/rbi/gems/{tapioca@0.16.11.rbi → tapioca@0.17.10.rbi} +721 -835
- data/sorbet/rbi/gems/thor@1.4.0.rbi +53 -53
- data/sorbet/rbi/gems/tsort@0.2.0.rbi +393 -0
- data/sorbet/rbi/gems/type_toolkit@0.0.5.rbi +49 -0
- data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +144 -143
- data/sorbet/rbi/gems/uri@1.1.1.rbi +7 -7
- data/sorbet/rbi/gems/vcr@6.3.1.rbi +53 -36
- data/sorbet/rbi/gems/webmock@3.25.1.rbi +38 -13
- data/sorbet/rbi/gems/zeitwerk@2.7.3.rbi +39 -272
- data/sorbet/rbi/shims/lib/roast/execution_context.rbi +3 -3
- data/tutorial/01_your_first_workflow/README.md +9 -5
- data/tutorial/01_your_first_workflow/configured_chat.rb +1 -1
- data/tutorial/02_chaining_cogs/README.md +2 -2
- data/tutorial/02_chaining_cogs/code_review.rb +1 -1
- data/tutorial/02_chaining_cogs/session_resumption.rb +1 -1
- data/tutorial/03_targets_and_params/README.md +1 -1
- data/tutorial/04_configuration_options/README.md +2 -2
- data/tutorial/08_iterative_workflows/README.md +1 -1
- data/tutorial/README.md +1 -1
- metadata +39 -17
- data/docs/AGENT_STEPS.md +0 -288
- data/docs/INSTRUMENTATION.md +0 -243
- data/docs/ITERATION_SYNTAX.md +0 -147
- data/docs/VALIDATION.md +0 -178
- data/lib/roast/nil_assertions.rb +0 -23
- /data/internal/documentation/{architectural-notes.md → comments/architectural-notes.md} +0 -0
- /data/internal/documentation/{doc-comments-external.md → comments/doc-comments-external.md} +0 -0
- /data/internal/documentation/{doc-comments-internal.md → comments/doc-comments-internal.md} +0 -0
- /data/internal/documentation/{doc-comments.md → comments/doc-comments.md} +0 -0
|
@@ -27,11 +27,366 @@ module Roast
|
|
|
27
27
|
format_unknown
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# Truncate long formatted tool-use strings to keep terminal output to generally one line, accounting for logger prefixing.
|
|
31
|
+
TRUNCATE_LIMIT = 45
|
|
32
|
+
|
|
30
33
|
private
|
|
31
34
|
|
|
35
|
+
# Truncates a string to at most TRUNCATE_LIMIT characters, appending
|
|
36
|
+
# "..." when the string is cut. The returned string — including the
|
|
37
|
+
# ellipsis — is guaranteed to be ≤ TRUNCATE_LIMIT characters.
|
|
38
|
+
#
|
|
39
|
+
# nil is coerced to "" (so callers needn't nil-check).
|
|
40
|
+
# Strings at or below the limit are returned unchanged.
|
|
41
|
+
#
|
|
42
|
+
# Input:
|
|
43
|
+
# str (String?) – the string to truncate
|
|
44
|
+
#
|
|
45
|
+
# Output: the (possibly shortened) string
|
|
46
|
+
#
|
|
47
|
+
# Examples:
|
|
48
|
+
# truncate("ls -la") # => "ls -la"
|
|
49
|
+
# truncate("a" * 50) # => "aaa...aaa..." (45 chars)
|
|
50
|
+
# truncate(nil) # => ""
|
|
51
|
+
#
|
|
52
|
+
#: (String?) -> String
|
|
53
|
+
def truncate(str)
|
|
54
|
+
s = str.to_s
|
|
55
|
+
s.length > TRUNCATE_LIMIT ? "#{s[0...TRUNCATE_LIMIT - 3]}..." : s
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Formats a Bash tool-use line.
|
|
59
|
+
#
|
|
60
|
+
# Input fields:
|
|
61
|
+
# :command (String) – shell command to execute [required]
|
|
62
|
+
# :description (String) – human-readable label [optional]
|
|
63
|
+
#
|
|
64
|
+
# Output: "BASH <command>", with " (<description>)" appended when
|
|
65
|
+
# :description is present. :command is truncated to TRUNCATE_LIMIT chars.
|
|
66
|
+
#
|
|
67
|
+
# Examples:
|
|
68
|
+
# BASH ls -la (List directory contents)
|
|
69
|
+
# BASH ls -la
|
|
70
|
+
#
|
|
32
71
|
#: () -> String
|
|
33
72
|
def format_bash
|
|
34
|
-
|
|
73
|
+
command = truncate(input[:command])
|
|
74
|
+
description = input[:description]
|
|
75
|
+
label = command.empty? ? "BASH" : "BASH #{command}"
|
|
76
|
+
description ? "#{label} (#{description})" : label
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Formats a Read tool-use line.
|
|
80
|
+
#
|
|
81
|
+
# Input fields:
|
|
82
|
+
# :file_path (String) – absolute path to read [required]
|
|
83
|
+
# :offset (Integer) – 1-based first line [optional]
|
|
84
|
+
# :limit (Integer) – max number of lines to read [optional]
|
|
85
|
+
#
|
|
86
|
+
# Output: "READ <file_path>", with a line range appended when :offset
|
|
87
|
+
# and/or :limit is given:
|
|
88
|
+
# :limit set → " (lines <start>–<end>)", start = :offset (default
|
|
89
|
+
# 1), end = start + :limit - 1
|
|
90
|
+
# :offset only → " (from line <offset>)" (reads to end of file)
|
|
91
|
+
# With neither, the bare "READ <file_path>".
|
|
92
|
+
#
|
|
93
|
+
# Examples:
|
|
94
|
+
# READ /app/models/user.rb (lines 30–80)
|
|
95
|
+
# READ /app/models/user.rb (from line 30)
|
|
96
|
+
# READ /app/models/user.rb
|
|
97
|
+
#
|
|
98
|
+
#: () -> String
|
|
99
|
+
def format_read
|
|
100
|
+
file_path, offset, limit = input.values_at(:file_path, :offset, :limit)
|
|
101
|
+
details = if limit
|
|
102
|
+
offset ||= 1
|
|
103
|
+
"lines #{offset}–#{offset + limit - 1}"
|
|
104
|
+
elsif offset
|
|
105
|
+
"from line #{offset}"
|
|
106
|
+
end
|
|
107
|
+
label = file_path.to_s.empty? ? "READ" : "READ #{file_path}"
|
|
108
|
+
details ? "#{label} (#{details})" : label
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Formats a Glob tool-use line.
|
|
112
|
+
#
|
|
113
|
+
# Input fields:
|
|
114
|
+
# :pattern (String) – glob pattern to match [required]
|
|
115
|
+
# :path (String) – directory to search in [optional]
|
|
116
|
+
#
|
|
117
|
+
# Output: "GLOB <pattern>", with " (in <path>)" appended when :path
|
|
118
|
+
# is present.
|
|
119
|
+
#
|
|
120
|
+
# Pattern is not truncated because glob expressions are typically short
|
|
121
|
+
# and meaningful in their entirety, unlike shell commands.
|
|
122
|
+
#
|
|
123
|
+
# Examples:
|
|
124
|
+
# GLOB **/*.rb (in lib/roast)
|
|
125
|
+
# GLOB **/*.rb
|
|
126
|
+
#
|
|
127
|
+
#: () -> String
|
|
128
|
+
def format_glob
|
|
129
|
+
pattern, path = input.values_at(:pattern, :path)
|
|
130
|
+
path ? "GLOB #{pattern} (in #{path})" : "GLOB #{pattern}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Formats a Grep tool-use line.
|
|
134
|
+
#
|
|
135
|
+
# Input fields:
|
|
136
|
+
# :pattern (String) – regex to search for [required]
|
|
137
|
+
# :path (String) – file or directory to search [optional]
|
|
138
|
+
# :glob (String) – filter files by glob [optional]
|
|
139
|
+
# :type (String) – filter files by type [optional]
|
|
140
|
+
# :"-i" (bool) – case-insensitive match [optional]
|
|
141
|
+
#
|
|
142
|
+
# Output: 'GREP "<pattern>"', then " <path>" when :path is present,
|
|
143
|
+
# then " (<modifiers>)" when any of :glob, :type, :"-i" are set —
|
|
144
|
+
# joined with " · " in that order. :pattern is truncated to
|
|
145
|
+
# TRUNCATE_LIMIT chars; other grep options (such as :"-n") are not
|
|
146
|
+
# displayed, as they shape grep's output rather than the search itself.
|
|
147
|
+
#
|
|
148
|
+
# Examples:
|
|
149
|
+
# GREP "def format" lib/roast (glob=*.rb · -i)
|
|
150
|
+
# GREP "TODO"
|
|
151
|
+
#
|
|
152
|
+
#: () -> String
|
|
153
|
+
def format_grep
|
|
154
|
+
pattern, path = input.values_at(:pattern, :path)
|
|
155
|
+
base = "GREP \"#{truncate(pattern)}\""
|
|
156
|
+
base = "#{base} #{path}" if path
|
|
157
|
+
modifiers = [
|
|
158
|
+
("glob=#{input[:glob]}" if input[:glob]),
|
|
159
|
+
("type=#{input[:type]}" if input[:type]),
|
|
160
|
+
("-i" if input[:"-i"]),
|
|
161
|
+
].compact.join(" · ")
|
|
162
|
+
modifiers.empty? ? base : "#{base} (#{modifiers})"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Formats a Write tool-use line.
|
|
166
|
+
#
|
|
167
|
+
# Input fields:
|
|
168
|
+
# :file_path (String) – absolute path to write [required]
|
|
169
|
+
# :content (String) – full file contents [required]
|
|
170
|
+
#
|
|
171
|
+
# Output: 'WRITE <file_path> "<preview>" (+<n> lines)', where
|
|
172
|
+
# <preview> is the first line of :content (stripped, truncated to
|
|
173
|
+
# TRUNCATE_LIMIT chars) and <n> is the total number of lines written
|
|
174
|
+
# (singular " line" when <n> is 1). The count is always shown.
|
|
175
|
+
#
|
|
176
|
+
# Examples:
|
|
177
|
+
# WRITE /app/models/user.rb "class User < ApplicationRecord" (+10 lines)
|
|
178
|
+
# WRITE /config/app.yml "enabled: true" (+1 line)
|
|
179
|
+
#
|
|
180
|
+
#: () -> String
|
|
181
|
+
def format_write
|
|
182
|
+
file_path, content = input.values_at(:file_path, :content)
|
|
183
|
+
lines = content.to_s.lines
|
|
184
|
+
preview = truncate(lines.first.to_s.strip)
|
|
185
|
+
count = lines.length
|
|
186
|
+
line_label = count == 1 ? "line" : "lines"
|
|
187
|
+
"WRITE #{file_path} \"#{preview}\" (+#{count} #{line_label})"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Formats an Edit tool-use line.
|
|
191
|
+
#
|
|
192
|
+
# Input fields:
|
|
193
|
+
# :file_path (String) – absolute path to edit [required]
|
|
194
|
+
# :old_string (String) – text being replaced [required]
|
|
195
|
+
# :new_string (String) – replacement text [required]
|
|
196
|
+
# :replace_all (bool) – replace every occurrence [optional]
|
|
197
|
+
#
|
|
198
|
+
# Output: "EDIT <file_path> (-<old> +<new> lines)", where <old> and
|
|
199
|
+
# <new> are the line counts of :old_string and :new_string. The
|
|
200
|
+
# count is always labeled " lines"; " · replace all" is appended
|
|
201
|
+
# when :replace_all is set.
|
|
202
|
+
#
|
|
203
|
+
# Examples:
|
|
204
|
+
# EDIT /app/models/user.rb (-6 +6 lines)
|
|
205
|
+
# EDIT /config/routes.rb (-1 +2 lines · replace all)
|
|
206
|
+
#
|
|
207
|
+
#: () -> String
|
|
208
|
+
def format_edit
|
|
209
|
+
file_path, old_string, new_string = input.values_at(:file_path, :old_string, :new_string)
|
|
210
|
+
old_count = old_string.to_s.lines.length
|
|
211
|
+
new_count = new_string.to_s.lines.length
|
|
212
|
+
details = "-#{old_count} +#{new_count} lines"
|
|
213
|
+
details = "#{details} · replace all" if input[:replace_all]
|
|
214
|
+
"EDIT #{file_path} (#{details})"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Formats a TodoWrite tool-use line.
|
|
218
|
+
#
|
|
219
|
+
# Input fields:
|
|
220
|
+
# :todos (Array) – the full todo list [required]
|
|
221
|
+
#
|
|
222
|
+
# Output: "TODOWRITE <n> todos" (singular "todo" when n is 1). When the
|
|
223
|
+
# list is non-empty, " (<breakdown>)" is appended — each distinct :status
|
|
224
|
+
# with its count ("<count> <status>"), joined with " · " in the order the
|
|
225
|
+
# statuses first appear.
|
|
226
|
+
#
|
|
227
|
+
# Examples:
|
|
228
|
+
# TODOWRITE 3 todos (2 completed · 1 in_progress)
|
|
229
|
+
# TODOWRITE 1 todo (1 in_progress)
|
|
230
|
+
# TODOWRITE 0 todos
|
|
231
|
+
#
|
|
232
|
+
#: () -> String
|
|
233
|
+
def format_todowrite
|
|
234
|
+
todos = input[:todos] || []
|
|
235
|
+
counts = todos.group_by { |todo| todo[:status] }.transform_values(&:length)
|
|
236
|
+
summary = counts.map { |status, count| "#{count} #{status}" }.join(" · ")
|
|
237
|
+
label = todos.length == 1 ? "todo" : "todos"
|
|
238
|
+
base = "TODOWRITE #{todos.length} #{label}"
|
|
239
|
+
summary.empty? ? base : "#{base} (#{summary})"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Formats a Skill tool-use line.
|
|
243
|
+
#
|
|
244
|
+
# Input fields:
|
|
245
|
+
# :skill (String) – name of the skill to invoke [required]
|
|
246
|
+
# :args (String) – arguments passed to the skill [optional]
|
|
247
|
+
#
|
|
248
|
+
# Output: "SKILL <skill>", with " (<args>)" appended when :args is
|
|
249
|
+
# present. :args is truncated to TRUNCATE_LIMIT chars; :skill is not.
|
|
250
|
+
#
|
|
251
|
+
# Examples:
|
|
252
|
+
# SKILL pr-description (draft for the auth changes)
|
|
253
|
+
# SKILL pr-description
|
|
254
|
+
#
|
|
255
|
+
#: () -> String
|
|
256
|
+
def format_skill
|
|
257
|
+
skill, args = input.values_at(:skill, :args)
|
|
258
|
+
args ? "SKILL #{skill} (#{truncate(args)})" : "SKILL #{skill}"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Formats a Task tool-use line.
|
|
262
|
+
#
|
|
263
|
+
# Input fields:
|
|
264
|
+
# :description (String) – short label for the task [required]
|
|
265
|
+
# :subagent_type (String) – which agent type to spawn [optional]
|
|
266
|
+
# :run_in_background (bool) – run the agent asynchronously [optional]
|
|
267
|
+
# :model (String) – model override for the agent [optional]
|
|
268
|
+
#
|
|
269
|
+
# Output: "TASK <description>", with " (<details>)" appended when any
|
|
270
|
+
# optional field is set: :subagent_type, then "background" (when
|
|
271
|
+
# :run_in_background is set), then :model — joined with " · " in that
|
|
272
|
+
# order and shown as raw values (no key= labels). :description is
|
|
273
|
+
# truncated to TRUNCATE_LIMIT chars; the other fields are not.
|
|
274
|
+
#
|
|
275
|
+
# Examples:
|
|
276
|
+
# TASK Find all callers (Explore · background · opus)
|
|
277
|
+
# TASK Summarize the diff
|
|
278
|
+
#
|
|
279
|
+
#: () -> String
|
|
280
|
+
def format_task
|
|
281
|
+
description = truncate(input[:description])
|
|
282
|
+
details = [
|
|
283
|
+
input[:subagent_type],
|
|
284
|
+
("background" if input[:run_in_background]),
|
|
285
|
+
input[:model],
|
|
286
|
+
].compact.join(" · ")
|
|
287
|
+
details.empty? ? "TASK #{description}" : "TASK #{description} (#{details})"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Formats an Agent tool-use line.
|
|
291
|
+
#
|
|
292
|
+
# Input fields:
|
|
293
|
+
# :description (String) – short label for the work [required]
|
|
294
|
+
# :subagent_type (String) – which agent type to spawn [optional]
|
|
295
|
+
# :run_in_background (bool) – run the agent asynchronously [optional]
|
|
296
|
+
#
|
|
297
|
+
# Output: "AGENT <description>", with " (<details>)" appended when any
|
|
298
|
+
# optional field is set: :subagent_type, then "background" (when
|
|
299
|
+
# :run_in_background is set) — joined with " · " in that order and shown
|
|
300
|
+
# as raw values (no key= labels). :description is truncated to
|
|
301
|
+
# TRUNCATE_LIMIT chars; :subagent_type is not.
|
|
302
|
+
#
|
|
303
|
+
# Examples:
|
|
304
|
+
# AGENT Find all callers (Explore · background)
|
|
305
|
+
# AGENT Summarize the diff
|
|
306
|
+
#
|
|
307
|
+
#: () -> String
|
|
308
|
+
def format_agent
|
|
309
|
+
description = truncate(input[:description])
|
|
310
|
+
details = [
|
|
311
|
+
input[:subagent_type],
|
|
312
|
+
("background" if input[:run_in_background]),
|
|
313
|
+
].compact.join(" · ")
|
|
314
|
+
details.empty? ? "AGENT #{description}" : "AGENT #{description} (#{details})"
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Formats a TaskUpdate tool-use line.
|
|
318
|
+
#
|
|
319
|
+
# Input fields:
|
|
320
|
+
# :taskId (Integer) – id of the task to update [required]
|
|
321
|
+
# :status (String) – the task's new status [required]
|
|
322
|
+
#
|
|
323
|
+
# Output: "TASKUPDATE #<taskId> → <status>" — the id is prefixed with
|
|
324
|
+
# "#" and joined to the status with " → ". Both fields are always
|
|
325
|
+
# shown and neither is truncated.
|
|
326
|
+
#
|
|
327
|
+
# Examples:
|
|
328
|
+
# TASKUPDATE #1 → completed
|
|
329
|
+
# TASKUPDATE #2 → in_progress
|
|
330
|
+
#
|
|
331
|
+
#: () -> String
|
|
332
|
+
def format_taskupdate
|
|
333
|
+
"TASKUPDATE ##{input[:taskId]} → #{input[:status]}"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Formats a TaskCreate tool-use line.
|
|
337
|
+
#
|
|
338
|
+
# Input fields:
|
|
339
|
+
# :subject (String) – title of the task to create [required]
|
|
340
|
+
#
|
|
341
|
+
# Output: "TASKCREATE <subject>", with :subject truncated to
|
|
342
|
+
# TRUNCATE_LIMIT chars.
|
|
343
|
+
#
|
|
344
|
+
# Examples:
|
|
345
|
+
# TASKCREATE Run formatter stress test
|
|
346
|
+
# TASKCREATE Review multiline edit output in tmp/formatter_st...
|
|
347
|
+
#
|
|
348
|
+
#: () -> String
|
|
349
|
+
def format_taskcreate
|
|
350
|
+
"TASKCREATE #{truncate(input[:subject])}"
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Formats a TaskOutput tool-use line.
|
|
354
|
+
#
|
|
355
|
+
# Input fields:
|
|
356
|
+
# :task_id (String) – id of the task to read [required]
|
|
357
|
+
# :block (bool) – wait for the task to complete [optional]
|
|
358
|
+
# :timeout (Integer) – max wait in milliseconds [optional]
|
|
359
|
+
#
|
|
360
|
+
# Output: "TASKOUTPUT #<task_id>", with " (<details>)" appended for any
|
|
361
|
+
# set options. :block renders its mode only when explicitly set — "sync"
|
|
362
|
+
# (true) or "async" (false) — and nothing when omitted. :timeout follows
|
|
363
|
+
# as "<n>s timeout" (whole seconds drop the decimal). Modifiers join with
|
|
364
|
+
# " · " in that order. :task_id is truncated to TRUNCATE_LIMIT chars.
|
|
365
|
+
#
|
|
366
|
+
# Examples:
|
|
367
|
+
# TASKOUTPUT #abc123 (sync · 120s timeout)
|
|
368
|
+
# TASKOUTPUT #abc123 (async · 5s timeout)
|
|
369
|
+
# TASKOUTPUT #abc123
|
|
370
|
+
#
|
|
371
|
+
#: () -> String
|
|
372
|
+
def format_taskoutput
|
|
373
|
+
task_id = truncate(input[:task_id])
|
|
374
|
+
timeout_ms = input[:timeout]
|
|
375
|
+
timeout_str = if timeout_ms
|
|
376
|
+
seconds = ActiveSupport::NumberHelper.number_to_rounded(
|
|
377
|
+
timeout_ms / 1000.0,
|
|
378
|
+
delimiter: "",
|
|
379
|
+
precision: 3,
|
|
380
|
+
strip_insignificant_zeros: true,
|
|
381
|
+
)
|
|
382
|
+
"#{seconds}s timeout"
|
|
383
|
+
end
|
|
384
|
+
block_label = case input[:block]
|
|
385
|
+
when true then "sync"
|
|
386
|
+
when false then "async"
|
|
387
|
+
end
|
|
388
|
+
details = [block_label, timeout_str].compact.join(" · ")
|
|
389
|
+
details.empty? ? "TASKOUTPUT ##{task_id}" : "TASKOUTPUT ##{task_id} (#{details})"
|
|
35
390
|
end
|
|
36
391
|
|
|
37
392
|
#: () -> String
|
|
@@ -18,9 +18,22 @@ module Roast
|
|
|
18
18
|
|
|
19
19
|
#: (Agent::Input) -> Agent::Output
|
|
20
20
|
def invoke(input)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
invocations = [] #: Array[ClaudeInvocation]
|
|
22
|
+
input.prompts.each do |prompt|
|
|
23
|
+
previous_session = invocations.last&.result&.session
|
|
24
|
+
invocation = ClaudeInvocation.new(
|
|
25
|
+
@config,
|
|
26
|
+
prompt,
|
|
27
|
+
previous_session || input.session,
|
|
28
|
+
fork_session: previous_session.nil?,
|
|
29
|
+
)
|
|
30
|
+
invocation.run!
|
|
31
|
+
invocations << invocation
|
|
32
|
+
break unless invocation.result.success
|
|
33
|
+
end
|
|
34
|
+
final_result = invocations.last.not_nil!.result
|
|
35
|
+
final_result.stats = invocations.filter_map { |i| i.result.stats }.reduce(:+) if invocations.size > 1
|
|
36
|
+
Output.new(final_result)
|
|
24
37
|
end
|
|
25
38
|
end
|
|
26
39
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module Cogs
|
|
6
|
+
class Agent < Cog
|
|
7
|
+
module Providers
|
|
8
|
+
class Pi < Provider
|
|
9
|
+
module Messages
|
|
10
|
+
# Represents a tool call made by the Pi agent
|
|
11
|
+
#
|
|
12
|
+
# In Pi's JSON protocol, tool calls appear as `toolcall_end` events within
|
|
13
|
+
# `message_update` messages, containing the tool name, id, and arguments.
|
|
14
|
+
class ToolCallMessage
|
|
15
|
+
#: String?
|
|
16
|
+
attr_reader :id
|
|
17
|
+
|
|
18
|
+
#: String?
|
|
19
|
+
attr_reader :name
|
|
20
|
+
|
|
21
|
+
#: Hash[Symbol, untyped]
|
|
22
|
+
attr_reader :arguments
|
|
23
|
+
|
|
24
|
+
#: (id: String?, name: String?, arguments: Hash[Symbol, untyped]) -> void
|
|
25
|
+
def initialize(id:, name:, arguments:)
|
|
26
|
+
@id = id
|
|
27
|
+
@name = name
|
|
28
|
+
@arguments = arguments
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#: () -> String?
|
|
32
|
+
def format
|
|
33
|
+
return unless name
|
|
34
|
+
|
|
35
|
+
case name.to_s.downcase
|
|
36
|
+
when "bash"
|
|
37
|
+
"BASH #{arguments[:command]}"
|
|
38
|
+
when "read"
|
|
39
|
+
"READ #{arguments[:path]}"
|
|
40
|
+
when "edit"
|
|
41
|
+
"EDIT #{arguments[:path]}"
|
|
42
|
+
when "write"
|
|
43
|
+
"WRITE #{arguments[:path]}"
|
|
44
|
+
when "grep"
|
|
45
|
+
"GREP #{arguments[:pattern]} #{arguments[:path]}"
|
|
46
|
+
when "find"
|
|
47
|
+
"FIND #{arguments[:pattern]} #{arguments[:path]}"
|
|
48
|
+
when "ls"
|
|
49
|
+
"LS #{arguments[:path]}"
|
|
50
|
+
else
|
|
51
|
+
"TOOL [#{name}] #{arguments.inspect}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module Cogs
|
|
6
|
+
class Agent < Cog
|
|
7
|
+
module Providers
|
|
8
|
+
class Pi < Provider
|
|
9
|
+
module Messages
|
|
10
|
+
# Represents a tool execution result from the Pi agent
|
|
11
|
+
#
|
|
12
|
+
# In Pi's JSON protocol, tool results appear as `tool_execution_end` events
|
|
13
|
+
# containing the result content and error status.
|
|
14
|
+
class ToolResultMessage
|
|
15
|
+
#: String?
|
|
16
|
+
attr_reader :tool_call_id
|
|
17
|
+
|
|
18
|
+
#: String?
|
|
19
|
+
attr_reader :tool_name
|
|
20
|
+
|
|
21
|
+
#: String?
|
|
22
|
+
attr_reader :content
|
|
23
|
+
|
|
24
|
+
#: bool
|
|
25
|
+
attr_reader :is_error
|
|
26
|
+
|
|
27
|
+
#: (tool_call_id: String?, tool_name: String?, content: String?, is_error: bool) -> void
|
|
28
|
+
def initialize(tool_call_id:, tool_name:, content:, is_error:)
|
|
29
|
+
@tool_call_id = tool_call_id
|
|
30
|
+
@tool_name = tool_name
|
|
31
|
+
@content = content
|
|
32
|
+
@is_error = is_error
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#: (PiInvocation::Context) -> String?
|
|
36
|
+
def format(context)
|
|
37
|
+
tool_call = context.tool_call(tool_call_id)
|
|
38
|
+
name = tool_name || tool_call&.name || "unknown"
|
|
39
|
+
status = is_error ? "ERROR" : "OK"
|
|
40
|
+
|
|
41
|
+
# Truncate long tool results for progress display
|
|
42
|
+
c = content
|
|
43
|
+
display_content = if c && c.length > 200
|
|
44
|
+
"#{c[0..197]}..."
|
|
45
|
+
else
|
|
46
|
+
c
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
"#{name.upcase} #{status}#{display_content ? " #{display_content}" : ""}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|