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
data/lib/roast/cogs/chat.rb
CHANGED
|
@@ -20,6 +20,14 @@ module Roast
|
|
|
20
20
|
#
|
|
21
21
|
# For tasks requiring local filesystem access or locally-configured tools, use the `agent` cog instead.
|
|
22
22
|
class Chat < Cog
|
|
23
|
+
# Raised when the LLM response hits the max token limit, indicating the output was
|
|
24
|
+
# truncated and should not be considered usable.
|
|
25
|
+
class MaxTokensExceededError < Cog::CogError; end
|
|
26
|
+
|
|
27
|
+
# Anthropic always sends a max_tokens value in the request payload with this fallback
|
|
28
|
+
# when the model metadata doesn't specify one (see ruby_llm's Anthropic::Chat#build_base_payload).
|
|
29
|
+
ANTHROPIC_DEFAULT_MAX_TOKENS = 4096
|
|
30
|
+
|
|
23
31
|
# The configuration object for this chat cog instance
|
|
24
32
|
#
|
|
25
33
|
#: Roast::Cogs::Chat::Config
|
|
@@ -42,36 +50,100 @@ module Roast
|
|
|
42
50
|
chat.messages[num_existing_messages..].each do |message|
|
|
43
51
|
case message.role
|
|
44
52
|
when :user
|
|
45
|
-
|
|
53
|
+
Event << { block: { header: "USER PROMPT", content: message.content } } if config.show_prompt?
|
|
46
54
|
when :assistant
|
|
47
|
-
|
|
55
|
+
Event << { block: { header: "LLM RESPONSE", content: message.content } } if config.show_response?
|
|
48
56
|
else
|
|
49
57
|
# No other message types are expected, but let's show them if they do appear
|
|
50
58
|
# but only the user has requested some form of output
|
|
51
|
-
|
|
59
|
+
Event << { block: { header: "UNKNOWN", content: message.content } } if config.show_prompt? || config.show_response?
|
|
52
60
|
end
|
|
53
61
|
end
|
|
54
62
|
if config.show_stats?
|
|
55
63
|
temperature = chat.instance_variable_get(:@temperature)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
lines = ["Model: #{response.model_id}"]
|
|
65
|
+
lines << "Temperature: #{format("%0.2f", temperature)}" if temperature
|
|
66
|
+
lines << "Input Tokens: #{response.input_tokens}"
|
|
67
|
+
lines << "Output Tokens: #{response.output_tokens}"
|
|
68
|
+
Event << { block: { header: "LLM STATS", content: lines.join("\n") } }
|
|
61
69
|
end
|
|
62
70
|
|
|
71
|
+
verify_response_not_truncated!(response)
|
|
72
|
+
|
|
63
73
|
Output.new(Session.from_chat(chat), response.content)
|
|
64
74
|
end
|
|
65
75
|
|
|
66
76
|
private
|
|
67
77
|
|
|
78
|
+
# Verify that the LLM response was not truncated by hitting the max token limit.
|
|
79
|
+
#
|
|
80
|
+
# ruby_llm does not expose a stop_reason/finish_reason from the provider response, so we
|
|
81
|
+
# detect truncation heuristically: if the output token count equals (or exceeds) the
|
|
82
|
+
# effective max token limit, the content was almost certainly cut off mid-generation.
|
|
83
|
+
#
|
|
84
|
+
# The effective limit is derived from the ruby_llm model registry, which is populated
|
|
85
|
+
# from models.dev and provider APIs. This lookup is decoupled from the chat instance's
|
|
86
|
+
# `assume_model_exists` setting — even when the chat call itself skips registry
|
|
87
|
+
# verification (the default), we still consult the registry for token-limit metadata.
|
|
88
|
+
#
|
|
89
|
+
# For Anthropic, the provider always sends max_tokens with a fallback of 4096 in the
|
|
90
|
+
# request payload, so if the model is not in the registry we replicate that fallback.
|
|
91
|
+
# For other providers, if the model is not in the registry, the check is skipped because
|
|
92
|
+
# we have no ceiling to compare against.
|
|
93
|
+
#
|
|
94
|
+
# Note: the `>=` boundary can produce false positives when output hits the limit exactly
|
|
95
|
+
# on a natural boundary. This is an unavoidable trade-off of the heuristic approach until
|
|
96
|
+
# ruby_llm exposes finish_reason.
|
|
97
|
+
#
|
|
98
|
+
#: (RubyLLM::Message) -> void
|
|
99
|
+
def verify_response_not_truncated!(response)
|
|
100
|
+
max_tokens = effective_max_tokens
|
|
101
|
+
return unless max_tokens
|
|
102
|
+
return unless response.output_tokens
|
|
103
|
+
|
|
104
|
+
if response.output_tokens >= max_tokens
|
|
105
|
+
raise MaxTokensExceededError,
|
|
106
|
+
"LLM response from #{response.model_id} was truncated at the max token limit " \
|
|
107
|
+
"(output: #{response.output_tokens} tokens, limit: #{max_tokens} tokens). " \
|
|
108
|
+
"The response content is likely incomplete and should not be used."
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Determine the effective max token limit for the chat request.
|
|
113
|
+
#
|
|
114
|
+
# Queries the ruby_llm model registry directly, independent of the chat instance's
|
|
115
|
+
# `assume_model_exists` setting. This ensures the token limit is available even when
|
|
116
|
+
# the chat call itself uses the default permissive mode.
|
|
117
|
+
#
|
|
118
|
+
# Returns nil if the model is not in the registry and the provider doesn't have a
|
|
119
|
+
# known default.
|
|
120
|
+
#
|
|
121
|
+
#: () -> Integer?
|
|
122
|
+
def effective_max_tokens
|
|
123
|
+
RubyLLM.models.find(config.valid_model, config.valid_provider!)&.max_tokens
|
|
124
|
+
rescue RubyLLM::ModelNotFoundError
|
|
125
|
+
# Anthropic always sends max_tokens with a 4096 fallback in the request payload
|
|
126
|
+
ANTHROPIC_DEFAULT_MAX_TOKENS if config.valid_provider! == :anthropic
|
|
127
|
+
end
|
|
128
|
+
|
|
68
129
|
# Get a RubyLLM context configured for this chat cog
|
|
69
130
|
#
|
|
70
131
|
#: () -> RubyLLM::Context
|
|
71
132
|
def ruby_llm_context
|
|
72
133
|
@ruby_llm_context ||= RubyLLM.context do |context|
|
|
73
|
-
|
|
74
|
-
|
|
134
|
+
case config.valid_provider!
|
|
135
|
+
when :openai
|
|
136
|
+
context.openai_api_key = config.valid_api_key!
|
|
137
|
+
context.openai_api_base = config.valid_base_url
|
|
138
|
+
when :anthropic
|
|
139
|
+
context.anthropic_api_key = config.valid_api_key!
|
|
140
|
+
context.anthropic_api_base = config.valid_base_url
|
|
141
|
+
when :perplexity
|
|
142
|
+
context.perplexity_api_key = config.valid_api_key!
|
|
143
|
+
when :gemini
|
|
144
|
+
context.gemini_api_key = config.valid_api_key!
|
|
145
|
+
context.gemini_api_base = config.valid_base_url
|
|
146
|
+
end
|
|
75
147
|
end
|
|
76
148
|
end
|
|
77
149
|
end
|
data/lib/roast/event.rb
CHANGED
data/lib/roast/event_monitor.rb
CHANGED
|
@@ -6,6 +6,8 @@ module Roast
|
|
|
6
6
|
extend self
|
|
7
7
|
include Kernel
|
|
8
8
|
|
|
9
|
+
BLOCK_SEPARATOR = ("─" * 40).freeze #: String
|
|
10
|
+
|
|
9
11
|
class EventMonitorError < StandardError; end
|
|
10
12
|
|
|
11
13
|
class EventMonitorAlreadyStartedError < EventMonitorError; end
|
|
@@ -116,17 +118,47 @@ module Roast
|
|
|
116
118
|
|
|
117
119
|
#: (Event) -> void
|
|
118
120
|
def handle_log_event(event)
|
|
119
|
-
|
|
121
|
+
path = format_path(event)
|
|
122
|
+
event.log_message.lines.each_with_index do |line, idx|
|
|
123
|
+
path_prefix = idx.zero? ? path : "·" * path.length
|
|
124
|
+
Roast::Log.logger.add(event.log_severity, "#{path_prefix} #{line.rstrip}")
|
|
125
|
+
end
|
|
120
126
|
end
|
|
121
127
|
|
|
122
128
|
#: (Event) -> void
|
|
123
129
|
def handle_stderr_event(event)
|
|
124
|
-
|
|
130
|
+
path = format_path(event)
|
|
131
|
+
event[:stderr].lines.each_with_index do |line, idx|
|
|
132
|
+
path_prefix = idx.zero? ? "#{path} ❯❯" : "#{"·" * path.length} ❙❙"
|
|
133
|
+
Roast::Log.logger.warn do
|
|
134
|
+
Roast::Log::Message.new(
|
|
135
|
+
type: :stderr,
|
|
136
|
+
text: "#{path_prefix} #{line.rstrip}",
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
125
140
|
end
|
|
126
141
|
|
|
127
142
|
#: (Event) -> void
|
|
128
143
|
def handle_stdout_event(event)
|
|
129
|
-
|
|
144
|
+
path = format_path(event)
|
|
145
|
+
event[:stdout].lines.each_with_index do |line, idx|
|
|
146
|
+
path_prefix = idx.zero? ? "#{path} ❯" : "#{"·" * path.length} ❙"
|
|
147
|
+
Roast::Log.logger.info do
|
|
148
|
+
Roast::Log::Message.new(
|
|
149
|
+
type: :stdout,
|
|
150
|
+
text: "#{path_prefix} #{line.rstrip}",
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
#: (Event) -> void
|
|
157
|
+
def handle_block_event(event)
|
|
158
|
+
block = event[:block]
|
|
159
|
+
header = "[#{block[:header]}]"
|
|
160
|
+
content = block[:content]
|
|
161
|
+
Roast::Log.logger.info { "#{format_path(event)} #{header}↓\n#{BLOCK_SEPARATOR}\n#{content}\n#{BLOCK_SEPARATOR}" }
|
|
130
162
|
end
|
|
131
163
|
|
|
132
164
|
#: (Event) -> void
|
data/lib/roast/log.rb
CHANGED
|
@@ -21,6 +21,27 @@ module Roast
|
|
|
21
21
|
extend self
|
|
22
22
|
include Kernel
|
|
23
23
|
|
|
24
|
+
# A log message paired with its originating type, so downstream handlers
|
|
25
|
+
# (e.g. the formatter) can render or route it by type.
|
|
26
|
+
class Message
|
|
27
|
+
#: Symbol
|
|
28
|
+
attr_reader :type
|
|
29
|
+
|
|
30
|
+
#: String
|
|
31
|
+
attr_reader :text
|
|
32
|
+
|
|
33
|
+
#: (type: Symbol, text: String) -> void
|
|
34
|
+
def initialize(type:, text:)
|
|
35
|
+
@type = type
|
|
36
|
+
@text = text
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
#: () -> String
|
|
40
|
+
def to_s
|
|
41
|
+
text
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
24
45
|
LOG_LEVELS = {
|
|
25
46
|
DEBUG: ::Logger::DEBUG,
|
|
26
47
|
INFO: ::Logger::INFO,
|
data/lib/roast/log_formatter.rb
CHANGED
|
@@ -15,22 +15,22 @@ module Roast
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def call(severity, time, _progname, msg)
|
|
18
|
+
type = msg.is_a?(Roast::Log::Message) ? msg.type : nil
|
|
18
19
|
line = if @tty
|
|
19
20
|
format(TTY_FORMAT, severity, msg2str(msg))
|
|
20
21
|
else
|
|
21
22
|
format(NON_TTY_FORMAT, severity, time.strftime(DATETIME_FORMAT), severity, msg2str(msg))
|
|
22
23
|
end
|
|
23
|
-
colourize(severity, line)
|
|
24
|
+
colourize(severity, type, line)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
private
|
|
27
28
|
|
|
28
|
-
#: (String, String) -> String
|
|
29
|
-
def colourize(severity, line)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@rainbow.wrap(line)
|
|
29
|
+
#: (String, Symbol?, String) -> String
|
|
30
|
+
def colourize(severity, type, line)
|
|
31
|
+
case type
|
|
32
|
+
when :stderr then @rainbow.wrap(line).yellow
|
|
33
|
+
when :stdout then @rainbow.wrap(line)
|
|
34
34
|
else
|
|
35
35
|
case severity
|
|
36
36
|
when "ERROR", "FATAL" then @rainbow.wrap(line).red
|
|
@@ -47,6 +47,8 @@ module Roast
|
|
|
47
47
|
msg = case msg
|
|
48
48
|
when ::String
|
|
49
49
|
msg.strip
|
|
50
|
+
when Roast::Log::Message
|
|
51
|
+
msg.text.strip
|
|
50
52
|
else
|
|
51
53
|
msg
|
|
52
54
|
end
|
data/lib/roast/version.rb
CHANGED
data/lib/roast.rb
CHANGED
|
@@ -34,9 +34,7 @@ require "async"
|
|
|
34
34
|
require "async/semaphore"
|
|
35
35
|
require "rainbow"
|
|
36
36
|
require "ruby_llm"
|
|
37
|
-
|
|
38
|
-
# Require project components that will not get automatically loaded
|
|
39
|
-
require "roast/nil_assertions"
|
|
37
|
+
require "type_toolkit"
|
|
40
38
|
|
|
41
39
|
# Autoloading setup
|
|
42
40
|
require "zeitwerk"
|
data/roast-ai.gemspec
CHANGED
|
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
spec.add_dependency("activesupport", "~> 8.0")
|
|
35
35
|
spec.add_dependency("async", ">= 2.34")
|
|
36
36
|
spec.add_dependency("rainbow", ">= 3.0.0")
|
|
37
|
-
spec.add_dependency("ruby_llm", ">= 1.
|
|
37
|
+
spec.add_dependency("ruby_llm", ">= 1.13")
|
|
38
|
+
spec.add_dependency("type_toolkit", ">= 0.0.5")
|
|
38
39
|
spec.add_dependency("zeitwerk", ">= 2.6")
|
|
39
40
|
end
|