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
|
@@ -53,20 +53,23 @@ module Roast
|
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
#: (Agent::Config,
|
|
57
|
-
def initialize(config,
|
|
56
|
+
#: (Agent::Config, String, String?, ?fork_session: bool) -> void
|
|
57
|
+
def initialize(config, prompt, session, fork_session: true)
|
|
58
58
|
@base_command = config.valid_command #: (String | Array[String])?
|
|
59
59
|
@model = config.valid_model #: String?
|
|
60
60
|
@append_system_prompt = config.valid_append_system_prompt #: String?
|
|
61
61
|
@replace_system_prompt = config.valid_replace_system_prompt #: String?
|
|
62
62
|
@apply_permissions = config.apply_permissions? #: bool
|
|
63
63
|
@working_directory = config.valid_working_directory #: Pathname?
|
|
64
|
-
@prompt = input.valid_prompt! #: String
|
|
65
|
-
@session = input.session #: String?
|
|
66
64
|
@context = Context.new #: Context
|
|
67
65
|
@result = Result.new #: Result
|
|
68
66
|
@raw_dump_file = config.valid_dump_raw_agent_messages_to_path #: Pathname?
|
|
67
|
+
@show_prompt = config.show_prompt? #: bool
|
|
69
68
|
@show_progress = config.show_progress? #: bool
|
|
69
|
+
@show_response = config.show_response? #: bool
|
|
70
|
+
@prompt = prompt
|
|
71
|
+
@session = session
|
|
72
|
+
@fork_session = fork_session #: bool
|
|
70
73
|
end
|
|
71
74
|
|
|
72
75
|
#: () -> void
|
|
@@ -74,6 +77,7 @@ module Roast
|
|
|
74
77
|
raise ClaudeAlreadyStartedError if started?
|
|
75
78
|
|
|
76
79
|
@started = true
|
|
80
|
+
Event << { block: { header: "USER PROMPT", content: @prompt } } if @show_prompt
|
|
77
81
|
_stdout, stderr, status = CommandRunner.execute(
|
|
78
82
|
command_line,
|
|
79
83
|
working_directory: @working_directory,
|
|
@@ -83,6 +87,7 @@ module Roast
|
|
|
83
87
|
|
|
84
88
|
if status.success?
|
|
85
89
|
@completed = true
|
|
90
|
+
Event << { block: { header: "AGENT RESPONSE", content: @result.response } } if @show_response
|
|
86
91
|
else
|
|
87
92
|
@failed = true
|
|
88
93
|
@result.success = false
|
|
@@ -180,7 +185,10 @@ module Roast
|
|
|
180
185
|
command.push("--model", @model) if @model
|
|
181
186
|
command.push("--system-prompt", @replace_system_prompt) if @replace_system_prompt
|
|
182
187
|
command.push("--append-system-prompt", @append_system_prompt) if @append_system_prompt
|
|
183
|
-
|
|
188
|
+
if @session.present?
|
|
189
|
+
command.push("--fork-session") if @fork_session
|
|
190
|
+
command.push("--resume", @session)
|
|
191
|
+
end
|
|
184
192
|
command << "--dangerously-skip-permissions" unless @apply_permissions
|
|
185
193
|
command
|
|
186
194
|
end
|
|
@@ -31,7 +31,7 @@ module Roast
|
|
|
31
31
|
@content = hash.delete(:result) || ""
|
|
32
32
|
@success = hash.delete(:success) || subtype == "success"
|
|
33
33
|
if hash.delete(:is_error) || subtype == "error"
|
|
34
|
-
@content = @content || hash.dig(:error, :message) || "Unknown error"
|
|
34
|
+
@content = @content.presence || hash.dig(:error, :message) || "Unknown error"
|
|
35
35
|
hash.delete(:error)
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -10,36 +10,376 @@ module Roast
|
|
|
10
10
|
#: Symbol?
|
|
11
11
|
attr_reader :tool_name
|
|
12
12
|
|
|
13
|
+
#: Hash[Symbol, untyped]
|
|
14
|
+
attr_reader :tool_use_input
|
|
15
|
+
|
|
13
16
|
#: String?
|
|
14
17
|
attr_reader :tool_use_description
|
|
15
18
|
|
|
16
|
-
#: String?
|
|
19
|
+
#: (String | Array[Hash[Symbol, untyped]])?
|
|
17
20
|
attr_reader :content
|
|
18
21
|
|
|
19
22
|
#: bool
|
|
20
23
|
attr_reader :is_error
|
|
21
24
|
|
|
22
|
-
#: (tool_use: Messages::ToolUseMessage?, content: String?, is_error: bool) -> void
|
|
25
|
+
#: (tool_use: Messages::ToolUseMessage?, content: (String | Array[Hash[Symbol, untyped]])?, is_error: bool) -> void
|
|
23
26
|
def initialize(tool_use:, content:, is_error:)
|
|
24
27
|
@tool_name = tool_use&.name || :unknown
|
|
25
|
-
@
|
|
28
|
+
@tool_use_input = tool_use&.input || {} #: Hash[Symbol, untyped]
|
|
29
|
+
@tool_use_description = @tool_use_input[:description] #: String?
|
|
26
30
|
@content = content
|
|
27
31
|
@is_error = is_error
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
#: () -> String
|
|
31
35
|
def format
|
|
36
|
+
return error_line if is_error
|
|
37
|
+
|
|
32
38
|
format_method_name = "format_#{tool_name}".to_sym
|
|
33
39
|
return send(format_method_name) if respond_to?(format_method_name, true)
|
|
34
40
|
|
|
35
41
|
format_unknown
|
|
36
42
|
end
|
|
37
43
|
|
|
44
|
+
TRUNCATE_LIMIT = 45
|
|
45
|
+
|
|
38
46
|
private
|
|
39
47
|
|
|
48
|
+
# Formats a Bash tool-result line.
|
|
49
|
+
#
|
|
50
|
+
# Content: the command's combined stdout/stderr, as a string.
|
|
51
|
+
#
|
|
52
|
+
# Output: "BASH OK <n> <line|lines> · <preview>" – <n> is the number of
|
|
53
|
+
# output lines (pluralized), and <preview> is the first line, stripped and
|
|
54
|
+
# truncated to TRUNCATE_LIMIT chars. The " · <preview>" suffix is omitted
|
|
55
|
+
# when the command produced no output.
|
|
56
|
+
#
|
|
57
|
+
# Examples:
|
|
58
|
+
# BASH OK 12 lines · Cloning into 'roast'...
|
|
59
|
+
# BASH OK 1 line · hello world
|
|
60
|
+
# BASH OK 0 lines
|
|
61
|
+
#
|
|
62
|
+
#: () -> String
|
|
63
|
+
def format_bash
|
|
64
|
+
lines = content.to_s.lines
|
|
65
|
+
count = lines.length
|
|
66
|
+
preview = truncate(lines.first.to_s.strip)
|
|
67
|
+
ok_line("#{count} #{"line".pluralize(count)}", preview)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Formats a Read tool-result line.
|
|
71
|
+
#
|
|
72
|
+
# Content: the file's contents, as a string.
|
|
73
|
+
#
|
|
74
|
+
# Output: "READ OK <n> <line|lines>" – <n> is the number of lines
|
|
75
|
+
# read, pluralized.
|
|
76
|
+
#
|
|
77
|
+
# Examples:
|
|
78
|
+
# READ OK 42 lines
|
|
79
|
+
# READ OK 1 line
|
|
80
|
+
# READ OK 0 lines
|
|
81
|
+
#
|
|
82
|
+
#: () -> String
|
|
83
|
+
def format_read
|
|
84
|
+
count = content.to_s.lines.length
|
|
85
|
+
ok_line("#{count} #{"line".pluralize(count)}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Formats a Glob tool-result line.
|
|
89
|
+
#
|
|
90
|
+
# Content: newline-separated matches. Lines starting with "/" are
|
|
91
|
+
# file paths; any other line is a status message (a no-match
|
|
92
|
+
# sentinel, a truncation notice, etc.).
|
|
93
|
+
#
|
|
94
|
+
# Output: "GLOB OK <n> <file|files> found" – <n> is the number of
|
|
95
|
+
# path lines. When paths were found and a status message is present,
|
|
96
|
+
# it is appended as a truncated "NOTE <message>" part. A zero-result
|
|
97
|
+
# run omits the NOTE.
|
|
98
|
+
#
|
|
99
|
+
# Examples:
|
|
100
|
+
# GLOB OK 12 files found
|
|
101
|
+
# GLOB OK 1 file found
|
|
102
|
+
# GLOB OK 0 files found
|
|
103
|
+
# GLOB OK 8 files found · NOTE Results truncated...
|
|
104
|
+
#
|
|
105
|
+
#: () -> String
|
|
106
|
+
def format_glob
|
|
107
|
+
lines = content.to_s.lines.map(&:strip).reject(&:empty?)
|
|
108
|
+
files, notes = lines.partition { |line| line.start_with?("/") }
|
|
109
|
+
count = files.length
|
|
110
|
+
note = "NOTE #{truncate(notes.join(" "))}" if files.any? && notes.any?
|
|
111
|
+
ok_line("#{count} #{"file".pluralize(count)} found", note)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Formats a Grep tool-result line.
|
|
115
|
+
#
|
|
116
|
+
# Content: newline-separated results. A line is a match when it
|
|
117
|
+
# starts with either a path segment containing "/" (a bare path or
|
|
118
|
+
# path:line:content), or an optional "file:" prefix followed by a
|
|
119
|
+
# "<digits>:" line number (line:content in single-file mode, or
|
|
120
|
+
# path:line:content for a root-level file with no "/"). Any other
|
|
121
|
+
# line is a status message (a no-match sentinel, a truncation
|
|
122
|
+
# notice, etc.).
|
|
123
|
+
#
|
|
124
|
+
# Output: "GREP OK <n> <match|matches>" – <n> is the number of match
|
|
125
|
+
# lines. When matches were found and a status message is present, it
|
|
126
|
+
# is appended as a truncated "NOTE <message>" part. A zero-result run
|
|
127
|
+
# omits the NOTE.
|
|
128
|
+
#
|
|
129
|
+
# Examples:
|
|
130
|
+
# GREP OK 12 matches
|
|
131
|
+
# GREP OK 1 match
|
|
132
|
+
# GREP OK 0 matches
|
|
133
|
+
# GREP OK 8 matches · NOTE Results truncated...
|
|
134
|
+
#
|
|
135
|
+
#: () -> String
|
|
136
|
+
def format_grep
|
|
137
|
+
lines = content.to_s.lines.map(&:strip).reject(&:empty?)
|
|
138
|
+
matches, notes = lines.partition { |line| line.match?(%r{\A\S+/}) || line.match?(/\A(?:\S+:)?\d+:/) }
|
|
139
|
+
count = matches.length
|
|
140
|
+
note = "NOTE #{truncate(notes.join(" "))}" if matches.any? && notes.any?
|
|
141
|
+
ok_line("#{count} #{"match".pluralize(count)}", note)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Formats a Write tool-result line.
|
|
145
|
+
#
|
|
146
|
+
# Input: :file_path – the path that was written.
|
|
147
|
+
#
|
|
148
|
+
# Output: "WRITE OK <file_path>" – the file path, omitted if the
|
|
149
|
+
# input has none.
|
|
150
|
+
#
|
|
151
|
+
# Examples:
|
|
152
|
+
# WRITE OK lib/roast/version.rb
|
|
153
|
+
# WRITE OK
|
|
154
|
+
#
|
|
155
|
+
#: () -> String
|
|
156
|
+
def format_write
|
|
157
|
+
ok_line(tool_use_input[:file_path])
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Formats an Edit tool-result line.
|
|
161
|
+
#
|
|
162
|
+
# Input: :file_path – the path that was edited.
|
|
163
|
+
#
|
|
164
|
+
# Output: "EDIT OK <file_path>" – the file path, omitted if the
|
|
165
|
+
# input has none.
|
|
166
|
+
#
|
|
167
|
+
# Examples:
|
|
168
|
+
# EDIT OK lib/roast/version.rb
|
|
169
|
+
# EDIT OK
|
|
170
|
+
#
|
|
171
|
+
#: () -> String
|
|
172
|
+
def format_edit
|
|
173
|
+
ok_line(tool_use_input[:file_path])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Formats a Skill tool-result line.
|
|
177
|
+
#
|
|
178
|
+
# Input: :skill – the name of the invoked skill.
|
|
179
|
+
#
|
|
180
|
+
# Output: "SKILL OK <skill>" – the skill name, omitted if the
|
|
181
|
+
# input has none.
|
|
182
|
+
#
|
|
183
|
+
# Examples:
|
|
184
|
+
# SKILL OK commit
|
|
185
|
+
# SKILL OK
|
|
186
|
+
#
|
|
187
|
+
#: () -> String
|
|
188
|
+
def format_skill
|
|
189
|
+
ok_line(tool_use_input[:skill])
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Formats a TodoWrite tool-result line.
|
|
193
|
+
#
|
|
194
|
+
# Input: :todos – the todo list; each item is a hash with :status
|
|
195
|
+
# ("pending"/"in_progress"/"completed"), :content, and :activeForm.
|
|
196
|
+
#
|
|
197
|
+
# Output: "TODOWRITE OK <done>/<total> done · <active>" – the
|
|
198
|
+
# completed count over the total, then the in-progress item's
|
|
199
|
+
# :activeForm (or :content), truncated. The " · <active>" part is
|
|
200
|
+
# omitted when nothing is in progress; an empty list yields a bare
|
|
201
|
+
# "TODOWRITE OK".
|
|
202
|
+
#
|
|
203
|
+
# Examples:
|
|
204
|
+
# TODOWRITE OK 3/8 done · Implementing the parser
|
|
205
|
+
# TODOWRITE OK 8/8 done
|
|
206
|
+
# TODOWRITE OK
|
|
207
|
+
#
|
|
208
|
+
#: () -> String
|
|
209
|
+
def format_todowrite
|
|
210
|
+
todos = tool_use_input[:todos] || []
|
|
211
|
+
done = todos.count { |todo| todo[:status] == "completed" }
|
|
212
|
+
active = todos.find { |todo| todo[:status] == "in_progress" }
|
|
213
|
+
progress = "#{done}/#{todos.length} done" if todos.any?
|
|
214
|
+
active_label = truncate(active[:activeForm] || active[:content]) if active
|
|
215
|
+
ok_line(progress, active_label)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Formats a TaskUpdate tool-result line.
|
|
219
|
+
#
|
|
220
|
+
# Content: the text the tool returned.
|
|
221
|
+
#
|
|
222
|
+
# Output: "TASKUPDATE OK <preview>" – the first line of content,
|
|
223
|
+
# stripped and truncated to TRUNCATE_LIMIT chars. The preview is
|
|
224
|
+
# omitted when there is no content.
|
|
225
|
+
#
|
|
226
|
+
# Examples:
|
|
227
|
+
# TASKUPDATE OK Task updated successfully
|
|
228
|
+
# TASKUPDATE OK
|
|
229
|
+
#
|
|
230
|
+
#: () -> String
|
|
231
|
+
def format_taskupdate
|
|
232
|
+
preview = truncate(content.to_s.lines.first.to_s.strip)
|
|
233
|
+
ok_line(preview)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Formats a TaskCreate tool-result line.
|
|
237
|
+
#
|
|
238
|
+
# Content: the text the tool returned.
|
|
239
|
+
#
|
|
240
|
+
# Output: "TASKCREATE OK <preview>" – the first line of content,
|
|
241
|
+
# stripped and truncated to TRUNCATE_LIMIT chars. The preview is
|
|
242
|
+
# omitted when there is no content.
|
|
243
|
+
#
|
|
244
|
+
# Examples:
|
|
245
|
+
# TASKCREATE OK Task created successfully
|
|
246
|
+
# TASKCREATE OK
|
|
247
|
+
#
|
|
248
|
+
#: () -> String
|
|
249
|
+
def format_taskcreate
|
|
250
|
+
preview = truncate(content.to_s.lines.first.to_s.strip)
|
|
251
|
+
ok_line(preview)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Formats an Agent tool-result line.
|
|
255
|
+
#
|
|
256
|
+
# Content: the subagent's final message, delivered as a list of
|
|
257
|
+
# content blocks and joined into text.
|
|
258
|
+
#
|
|
259
|
+
# Output: "AGENT OK <preview>" – the first line of that text,
|
|
260
|
+
# stripped and truncated to TRUNCATE_LIMIT chars. The preview is
|
|
261
|
+
# omitted when there is no content.
|
|
262
|
+
#
|
|
263
|
+
# Examples:
|
|
264
|
+
# AGENT OK Refactored the parser; all tests pass
|
|
265
|
+
# AGENT OK Migrated the user table and backfilled all...
|
|
266
|
+
# AGENT OK
|
|
267
|
+
#
|
|
268
|
+
#: () -> String
|
|
269
|
+
def format_agent
|
|
270
|
+
preview = truncate(normalize_content(content).lines.first.to_s.strip)
|
|
271
|
+
ok_line(preview)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Formats a Task tool-result line.
|
|
275
|
+
#
|
|
276
|
+
# Content: the dispatched subagent's reply, or a launch notice
|
|
277
|
+
# when the run is backgrounded – delivered as a list of content
|
|
278
|
+
# blocks and joined into text.
|
|
279
|
+
#
|
|
280
|
+
# Output: "TASK OK <preview>" – the first line of that text,
|
|
281
|
+
# stripped and truncated to TRUNCATE_LIMIT chars. The preview is
|
|
282
|
+
# omitted when there is no content.
|
|
283
|
+
#
|
|
284
|
+
# Examples:
|
|
285
|
+
# TASK OK Async agent launched successfully
|
|
286
|
+
# TASK OK Migrated the user table and backfilled all...
|
|
287
|
+
# TASK OK
|
|
288
|
+
#
|
|
289
|
+
#: () -> String
|
|
290
|
+
def format_task
|
|
291
|
+
preview = truncate(normalize_content(content).lines.first.to_s.strip)
|
|
292
|
+
ok_line(preview)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Formats a TaskOutput tool-result line.
|
|
296
|
+
#
|
|
297
|
+
# Content: the polled task's payload – an XML-ish string carrying a
|
|
298
|
+
# <status> (or <retrieval_status>) tag and an <output> body.
|
|
299
|
+
#
|
|
300
|
+
# Output: "TASKOUTPUT OK <status>" – the text inside <status>, or
|
|
301
|
+
# inside <retrieval_status> when <status> is absent. The status is
|
|
302
|
+
# omitted when neither tag is present, leaving a bare "TASKOUTPUT OK".
|
|
303
|
+
#
|
|
304
|
+
# Examples:
|
|
305
|
+
# TASKOUTPUT OK completed
|
|
306
|
+
# TASKOUTPUT OK pending
|
|
307
|
+
# TASKOUTPUT OK
|
|
308
|
+
#
|
|
309
|
+
#: () -> String
|
|
310
|
+
def format_taskoutput
|
|
311
|
+
ok_line(tag_text("status") || tag_text("retrieval_status"))
|
|
312
|
+
end
|
|
313
|
+
|
|
40
314
|
#: () -> String
|
|
41
315
|
def format_unknown
|
|
42
|
-
"UNKNOWN [#{tool_name}]
|
|
316
|
+
"UNKNOWN [#{tool_name}] OK #{tool_use_description}\n#{content}"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Renders "<TOOL> OK[ <part> · <part> · ...]"; the success-side twin of
|
|
320
|
+
# #error_line. Blank/nil parts are dropped and the rest joined with " · ",
|
|
321
|
+
# so callers pass each piece of the summary without minding separators.
|
|
322
|
+
#
|
|
323
|
+
#: (*String?) -> String
|
|
324
|
+
def ok_line(*parts)
|
|
325
|
+
summary = parts.select(&:present?).join(" · ")
|
|
326
|
+
prefix = "#{tool_name.to_s.upcase} OK"
|
|
327
|
+
summary.present? ? "#{prefix} #{summary}" : prefix
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Renders "<TOOL> ERROR <message>" – the text inside the
|
|
331
|
+
# <tool_use_error> wrapper, or the whole content when it is unwrapped.
|
|
332
|
+
#
|
|
333
|
+
# Reads the instance's `content` and `tool_name` to produce a single-line
|
|
334
|
+
# error summary. Error messages are intentionally NOT truncated so the full
|
|
335
|
+
# diagnostic is preserved for debugging.
|
|
336
|
+
#
|
|
337
|
+
# Examples:
|
|
338
|
+
# BASH ERROR File has not been read yet.
|
|
339
|
+
# UNKNOWN ERROR command not found
|
|
340
|
+
#
|
|
341
|
+
#: () -> String
|
|
342
|
+
def error_line
|
|
343
|
+
message = tag_text("tool_use_error") || content.to_s.strip
|
|
344
|
+
"#{tool_name.to_s.upcase} ERROR #{message}".strip
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Returns the stripped text inside the first <tag>…</tag> pair in the
|
|
348
|
+
# result content, or nil when the tag is absent. The body is captured
|
|
349
|
+
# verbatim by a non-greedy match, so — unlike an XML parser — a body
|
|
350
|
+
# that itself contains bare angle brackets (such as an error message)
|
|
351
|
+
# is extracted intact.
|
|
352
|
+
#
|
|
353
|
+
# Matches a single, flat element: the lazy capture stops at the first
|
|
354
|
+
# closing </tag>, and nested same-name tags are not handled.
|
|
355
|
+
#
|
|
356
|
+
#: (String) -> String?
|
|
357
|
+
def tag_text(tag)
|
|
358
|
+
content.to_s[%r{<#{Regexp.escape(tag)}>(.*?)</#{Regexp.escape(tag)}>}m, 1]&.strip
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Truncates to TRUNCATE_LIMIT chars, appending "..." when cut. nil -> "".
|
|
362
|
+
#
|
|
363
|
+
#: (String?) -> String
|
|
364
|
+
def truncate(str)
|
|
365
|
+
s = str.to_s
|
|
366
|
+
s.length > TRUNCATE_LIMIT ? "#{s[0...TRUNCATE_LIMIT - 3]}..." : s
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# The result's text. Agent and Task deliver a list of content
|
|
370
|
+
# blocks, which is joined into a string; every other shape is
|
|
371
|
+
# coerced with to_s.
|
|
372
|
+
#
|
|
373
|
+
#: ((String | Array[Hash[Symbol, untyped]])?) -> String
|
|
374
|
+
def normalize_content(value)
|
|
375
|
+
case value
|
|
376
|
+
when String
|
|
377
|
+
value
|
|
378
|
+
when Array
|
|
379
|
+
value.filter_map { |b| b[:text] }.join("\n")
|
|
380
|
+
else
|
|
381
|
+
value.to_s
|
|
382
|
+
end
|
|
43
383
|
end
|
|
44
384
|
end
|
|
45
385
|
end
|