rails_console_ai 0.21.0 → 0.22.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8faec693c3d3c83dafcaa65437081d5ba8e235f04527a28182104c4ecba5b80d
4
- data.tar.gz: 171445a3ddc0c50093b1688260545df1558548a29cb81a6d3262d15d8ec4cf3c
3
+ metadata.gz: dba28b6d7543df66792877bddc51336f105c441a06b089de34ab0c7f89ca8f26
4
+ data.tar.gz: 4a54272fafd704abd003f06f2ab065d11f47b7c9a42b2f7c99c55b07b8712f8a
5
5
  SHA512:
6
- metadata.gz: 1d0689ebfc82073deb58f8bd03fce39503541d63f042f531322bfc11ef0d6cf65f69fa9ae59c8b89a8a549ea9b4b7893e8505da83887ec4cbc5c1a064ffa5d99
7
- data.tar.gz: e8f635c26035fcf73f2ccda5bd3d403a2ec33ef806c701c28f67ee8888a8189625a2569f45c369f7b29189fda3b162f03067be26590afbe5428be9da15487552
6
+ metadata.gz: 27c8413cc63c84c94d0fad464c3f3d6cd0b376a744122a2358b5899e0382f82e696a3e188647db952235986ee2a973c85b73065939a898cd7229955a61c377cf
7
+ data.tar.gz: d7e3041e7e37dd2ed304537bdfb02693647d3d32a9f1b6888718d9e38fcafe0c1b92000606c970716cd5074e7424b81e37d894d099b93ac6155268703ad54738
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.22.0]
6
+
7
+ - Fix blank content block handling in Bedrock provider
8
+ - Fix `recall_output` to expand in place and restore preview after LLM responds, preventing context bloat
9
+ - Remove safety guard bypass from prompt
10
+ - Fix issue where LLM couldn't recall multiple outputs at once
11
+
5
12
  ## [0.21.0]
6
13
 
7
14
  - Add Slack @mention support and channel name tracking
@@ -82,9 +82,6 @@ module RailsConsoleAi
82
82
  (e.g. `api = SalesforceApi.new(step1)`).
83
83
  - If the user asks you to provide code for them to run later (not execute now), put it
84
84
  in a ```ruby code block in your text response.
85
- - Use `RailsConsoleAi.configuration.safety_guards.without_guards { }` to wrap any
86
- operation that should bypass safety guards (e.g. calling a known-safe admin method).
87
-
88
85
  You have skills — predefined procedures for specific operations. When a user's request
89
86
  matches a skill, call activate_skill first to load the recipe and enable its guard
90
87
  bypasses, then follow the recipe.
@@ -242,11 +242,16 @@ module RailsConsoleAi
242
242
  output_parts << "Return value: #{exec_result.inspect}" if exec_result
243
243
 
244
244
  result_str = output_parts.join("\n\n")
245
- result_str = result_str[0..1000] + '...' if result_str.length > 1000
246
245
 
247
246
  context_msg = "User directly executed code: `#{raw_code}`"
248
- context_msg += "\n#{result_str}" unless output_parts.empty?
249
- output_id = output_parts.empty? ? nil : @executor.store_output(result_str)
247
+ if result_str.length > LARGE_OUTPUT_THRESHOLD
248
+ output_id = @executor.store_output(result_str)
249
+ preview = result_str[0, LARGE_OUTPUT_PREVIEW_CHARS]
250
+ context_msg += "\n#{preview}\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{result_str.length} chars — use recall_output tool with id #{output_id} to retrieve the full output]"
251
+ elsif !output_parts.empty?
252
+ output_id = @executor.store_output(result_str)
253
+ context_msg += "\n#{result_str}"
254
+ end
250
255
  @history << { role: :user, content: context_msg, output_id: output_id }
251
256
 
252
257
  @interactive_query ||= "> #{raw_code}"
@@ -314,9 +319,15 @@ module RailsConsoleAi
314
319
  output_parts << "Return value: #{exec_result.inspect}" if exec_result
315
320
  unless output_parts.empty?
316
321
  result_str = output_parts.join("\n\n")
317
- result_str = result_str[0..1000] + '...' if result_str.length > 1000
318
322
  output_id = @executor.store_output(result_str)
319
- @history << { role: :user, content: "Code was executed (safety override). #{result_str}", output_id: output_id }
323
+ context_msg = "Code was executed (safety override). "
324
+ if result_str.length > LARGE_OUTPUT_THRESHOLD
325
+ context_msg += result_str[0, LARGE_OUTPUT_PREVIEW_CHARS]
326
+ context_msg += "\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{result_str.length} chars — use recall_output tool with id #{output_id} to retrieve the full output]"
327
+ else
328
+ context_msg += result_str
329
+ end
330
+ @history << { role: :user, content: context_msg, output_id: output_id }
320
331
  end
321
332
  :success
322
333
  else
@@ -336,9 +347,15 @@ module RailsConsoleAi
336
347
 
337
348
  unless output_parts.empty?
338
349
  result_str = output_parts.join("\n\n")
339
- result_str = result_str[0..1000] + '...' if result_str.length > 1000
340
350
  output_id = @executor.store_output(result_str)
341
- @history << { role: :user, content: "Code was executed. #{result_str}", output_id: output_id }
351
+ context_msg = "Code was executed. "
352
+ if result_str.length > LARGE_OUTPUT_THRESHOLD
353
+ context_msg += result_str[0, LARGE_OUTPUT_PREVIEW_CHARS]
354
+ context_msg += "\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{result_str.length} chars — use recall_output tool with id #{output_id} to retrieve the full output]"
355
+ else
356
+ context_msg += result_str
357
+ end
358
+ @history << { role: :user, content: context_msg, output_id: output_id }
342
359
  end
343
360
 
344
361
  :success
@@ -615,6 +632,8 @@ module RailsConsoleAi
615
632
 
616
633
  This session has safety guards that block side effects (database writes, HTTP mutations, etc.).
617
634
  If an operation is blocked, the user will be prompted to allow it or disable guards.
635
+ Do NOT attempt to bypass or work around safety guards in your code — just write the
636
+ operation normally and let the safety system handle it.
618
637
  PROMPT
619
638
  end
620
639
  end
@@ -803,6 +822,28 @@ module RailsConsoleAi
803
822
  last_tool_names = result.tool_calls.map { |tc| tc[:name] }
804
823
  result.tool_calls.each do |tc|
805
824
  break if @channel.cancelled?
825
+
826
+ # Intercept recall_output/recall_outputs: expand in place instead of adding large messages
827
+ if tc[:name] == 'recall_output' || tc[:name] == 'recall_outputs'
828
+ ids = if tc[:name] == 'recall_outputs'
829
+ Array(tc[:arguments]['ids']).map(&:to_i)
830
+ else
831
+ [tc[:arguments]['id'].to_i]
832
+ end
833
+ @channel.display_tool_call("#{tc[:name]}(#{ids.join(', ')})")
834
+ expanded = expand_outputs_in_place(messages, ids)
835
+ tool_result = if expanded.any?
836
+ "Expanded #{expanded.length} output(s) in conversation. The full content is now visible in the original message(s) above."
837
+ else
838
+ "No matching outputs found with id(s) #{ids.join(', ')}."
839
+ end
840
+ @channel.display_dim(" #{tool_result}")
841
+ tool_msg = provider.format_tool_result(tc[:id], tool_result)
842
+ messages << tool_msg
843
+ new_messages << tool_msg
844
+ next
845
+ end
846
+
806
847
  if tc[:name] == 'ask_user' || tc[:name] == 'execute_plan'
807
848
  # Display any pending LLM text before prompting the user
808
849
  if last_thinking
@@ -848,6 +889,10 @@ module RailsConsoleAi
848
889
  exhausted = true if round == max_rounds - 1
849
890
  end
850
891
 
892
+ # Re-truncate any outputs that were expanded for the LLM — the LLM has
893
+ # seen them and responded, so collapse back to save context on future calls.
894
+ re_truncate_expanded(messages)
895
+
851
896
  if exhausted
852
897
  $stdout.puts "\e[33m Hit tool round limit (#{max_rounds}). Forcing final answer. Increase with: RailsConsoleAi.configure { |c| c.max_tool_rounds = 200 }\e[0m"
853
898
  messages << { role: :user, content: "You've used all available tool rounds. Please provide your best answer now based on what you've learned so far." }
@@ -1110,16 +1155,14 @@ module RailsConsoleAi
1110
1155
  .select { |m, _| m[:output_id] }
1111
1156
  .map { |_, i| i }
1112
1157
 
1113
- if output_indices.length <= RECENT_OUTPUTS_TO_KEEP
1114
- return messages.map { |m| m.except(:output_id) }
1115
- end
1158
+ return messages if output_indices.length <= RECENT_OUTPUTS_TO_KEEP
1116
1159
 
1117
1160
  trim_indices = output_indices[0..-(RECENT_OUTPUTS_TO_KEEP + 1)]
1118
1161
  messages.each_with_index.map do |msg, i|
1119
1162
  if trim_indices.include?(i)
1120
1163
  trim_message(msg)
1121
1164
  else
1122
- msg.except(:output_id)
1165
+ msg
1123
1166
  end
1124
1167
  end
1125
1168
  end
@@ -1135,12 +1178,52 @@ module RailsConsoleAi
1135
1178
  block
1136
1179
  end
1137
1180
  end
1138
- { role: msg[:role], content: trimmed_content }
1181
+ msg.merge(content: trimmed_content)
1139
1182
  elsif msg[:role].to_s == 'tool'
1140
- msg.except(:output_id).merge(content: ref)
1183
+ msg.merge(content: ref)
1141
1184
  else
1142
1185
  first_line = msg[:content].to_s.lines.first&.strip || msg[:content]
1143
- { role: msg[:role], content: "#{first_line}\n#{ref}" }
1186
+ msg.merge(content: "#{first_line}\n#{ref}")
1187
+ end
1188
+ end
1189
+
1190
+ def expand_outputs_in_place(messages, ids)
1191
+ expanded = []
1192
+ messages.each do |msg|
1193
+ next unless msg[:output_id] && ids.include?(msg[:output_id])
1194
+ full_output = @executor.recall_output(msg[:output_id])
1195
+ next unless full_output
1196
+ # Save original content so re_truncate_expanded can restore it
1197
+ msg[:pre_expand_content] = msg[:content]
1198
+ # Replace content with full output (handle Anthropic, OpenAI, and user message formats)
1199
+ if msg[:content].is_a?(Array)
1200
+ msg[:content] = msg[:content].map do |block|
1201
+ if block.is_a?(Hash) && block['type'] == 'tool_result'
1202
+ block.merge('content' => full_output)
1203
+ else
1204
+ block
1205
+ end
1206
+ end
1207
+ elsif msg[:role].to_s == 'tool'
1208
+ msg[:content] = full_output
1209
+ else
1210
+ # User messages (e.g., direct execution) — preserve first line, replace rest
1211
+ first_line = msg[:content].to_s.lines.first&.chomp || ''
1212
+ msg[:content] = "#{first_line}\n#{full_output}"
1213
+ end
1214
+ msg[:expanded] = true
1215
+ expanded << msg[:output_id]
1216
+ end
1217
+ expanded
1218
+ end
1219
+
1220
+ # Restore messages that were temporarily expanded back to their original
1221
+ # (preview/truncated) content. Called after the LLM has seen the expanded
1222
+ # content and responded.
1223
+ def re_truncate_expanded(messages)
1224
+ messages.each do |msg|
1225
+ next unless msg.delete(:expanded)
1226
+ msg[:content] = msg.delete(:pre_expand_content)
1144
1227
  end
1145
1228
  end
1146
1229
 
@@ -121,6 +121,8 @@ module RailsConsoleAi
121
121
  else
122
122
  [{ text: msg[:content].to_s }]
123
123
  end
124
+ # Bedrock rejects empty text blocks in content arrays
125
+ content.reject! { |block| block.is_a?(Hash) && block.key?(:text) && !block.key?(:tool_use) && !block.key?(:tool_result) && block[:text].to_s.empty? }
124
126
  { role: msg[:role].to_s, content: content }
125
127
  end
126
128
 
@@ -186,7 +186,7 @@ module RailsConsoleAi
186
186
  if @executor
187
187
  register(
188
188
  name: 'recall_output',
189
- description: 'Retrieve a previous code execution output that was omitted from the conversation to save context. Use the output id shown in the "[Output omitted]" placeholder.',
189
+ description: 'Retrieve a previous code execution output that was omitted or truncated. The output will be expanded in place in the conversation. Use the output id shown in the "[Output omitted]" or "[Output truncated]" placeholder.',
190
190
  parameters: {
191
191
  'type' => 'object',
192
192
  'properties' => {
@@ -199,6 +199,19 @@ module RailsConsoleAi
199
199
  result || "No output found with id #{args['id']}"
200
200
  }
201
201
  )
202
+
203
+ register(
204
+ name: 'recall_outputs',
205
+ description: 'Retrieve multiple previous code execution outputs that were omitted from the conversation. Use the output ids shown in "[Output omitted]" or "[Output truncated]" placeholders.',
206
+ parameters: {
207
+ 'type' => 'object',
208
+ 'properties' => {
209
+ 'ids' => { 'type' => 'array', 'items' => { 'type' => 'integer' }, 'description' => 'The output ids to retrieve' }
210
+ },
211
+ 'required' => ['ids']
212
+ },
213
+ handler: ->(args) { "recall_outputs handled by conversation engine" }
214
+ )
202
215
  end
203
216
 
204
217
  unless @mode == :init
@@ -1,3 +1,3 @@
1
1
  module RailsConsoleAi
2
- VERSION = '0.21.0'.freeze
2
+ VERSION = '0.22.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_console_ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.0
4
+ version: 0.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cortfr