console_agent 0.8.0 → 0.9.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: fdcfb3c48b2f8421b2187980a453b80324e6dec40ab80e8e55aa1a938355c79c
4
- data.tar.gz: 12fec02740fde7a87bb81e26dd6857263c96f9ebe5a8402040c72279e882e31f
3
+ metadata.gz: 5f82de0e3bdd6d1e3189c3b1bd0c88c386a6b31b4dff317656365352490f06c9
4
+ data.tar.gz: 1347a93e946c254809cdf6bfa90e3dea0378b7aef9a5e04950bdf0151569cd79
5
5
  SHA512:
6
- metadata.gz: 7af9a3c4fdbdf71abb7452d8e6747ba2b22c64cd08d123b761c5ca7ebb870c3ba9a01e458defe0308dcbf9435ec71879f7f3562503defdfd54e2026d3069679d
7
- data.tar.gz: c84e401d6b6f5c6c7840b5d701d3a0e653ba3ea46651141ae5101b2c93bdca35487566786a87e284dbba97f902c5dad597a00b8dc9fce6e1c01885b01e99a48d
6
+ metadata.gz: cec5e56aaddea19c5d1b9046fcbd7fd4707d16efa3fab65f9977703e24d28d177c12331c9cba7590048b1ad2f412d67bcb9ffe78c5cef288d6da40916cb53a86
7
+ data.tar.gz: 9e67f0c30bbd38561c400523ca19240f931ff5c08b353fa545037472b6b2195f931b5987b21d3c643a35e2cb3377647447cc676aa7544ba5a2ccec2a505f7e16
data/CHANGELOG.md ADDED
@@ -0,0 +1,58 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.8.0]
6
+
7
+ - Add authentication function support so host apps can avoid using basic auth
8
+ - Add `/think` and `/cost` commands with Sonnet vs Opus support
9
+ - Gracefully handle token limit exceeded errors
10
+
11
+ ## [0.7.0]
12
+
13
+ - Include binding variables and their classes in the Rails console context
14
+ - Add `ai_setup` command
15
+ - Add `/compact` mechanism for conversation management
16
+ - Catch errors and attempt to auto-fix them
17
+
18
+ ## [0.6.0]
19
+
20
+ - Add core memory (`console_agent.md`) that persists across sessions in the system prompt
21
+ - Add `ai_init` command to seed core memory
22
+ - Allow reading partial files
23
+ - Fix rspec hanging issues
24
+
25
+ ## [0.5.0]
26
+
27
+ - Auto-accept single-step plans
28
+ - Support `>` shorthand to run code directly
29
+ - Add `script/release` for releases
30
+
31
+ ## [0.4.0]
32
+
33
+ - Fix resuming sessions repeatedly
34
+ - Fix terminal flashing/loading in production (kubectl)
35
+ - Better escaping during thinking output
36
+
37
+ ## [0.3.0]
38
+
39
+ - Add plan mechanism with "auto" execution mode
40
+ - Add session logging to DB with `/console_agent` admin UI
41
+ - List and resume past sessions with pagination
42
+ - Add shift-tab for auto-execute mode
43
+ - Add usage display and debug toggle
44
+ - Store sessions incrementally; improved code segment display
45
+
46
+ ## [0.2.0]
47
+
48
+ - Add memory system with individual file storage
49
+ - Add `ask_user` tool
50
+ - Add registry cache
51
+ - Fix REPL up-key and ctrl-a navigation
52
+ - Show tool usage and model processing info
53
+ - Add token count information and debug ability
54
+ - Use tools-based approach instead of sending everything at once
55
+
56
+ ## [0.1.0]
57
+
58
+ - Initial implementation
@@ -13,7 +13,10 @@ module ConsoleAgent
13
13
  username = ConsoleAgent.configuration.admin_username
14
14
  password = ConsoleAgent.configuration.admin_password
15
15
 
16
- return unless username && password
16
+ unless username && password
17
+ head :unauthorized
18
+ return
19
+ end
17
20
 
18
21
  authenticate_or_request_with_http_basic('ConsoleAgent Admin') do |u, p|
19
22
  ActiveSupport::SecurityUtils.secure_compare(u, username) &
@@ -84,7 +84,7 @@ module ConsoleAgent
84
84
  result = binding_context.eval(code, "(console_agent)", 1)
85
85
 
86
86
  $stdout = old_stdout
87
- $stdout.puts colorize("=> #{result.inspect}", :green)
87
+ display_result(result)
88
88
 
89
89
  @last_output = captured_output.string
90
90
  result
@@ -126,35 +126,69 @@ module ConsoleAgent
126
126
  @last_answer = answer
127
127
  echo_stdin(answer)
128
128
 
129
- case answer
130
- when 'y', 'yes'
131
- execute(code)
132
- when 'e', 'edit'
133
- edited = open_in_editor(code)
134
- if edited && edited != code
135
- $stdout.puts colorize("# Edited code:", :yellow)
136
- $stdout.puts highlight_code(edited)
137
- $stdout.print colorize("Execute edited code? [y/N] ", :yellow)
138
- edit_answer = $stdin.gets.to_s.strip.downcase
139
- echo_stdin(edit_answer)
140
- if edit_answer == 'y'
141
- execute(edited)
129
+ loop do
130
+ case answer
131
+ when 'y', 'yes', 'a'
132
+ return execute(code)
133
+ when 'e', 'edit'
134
+ edited = open_in_editor(code)
135
+ if edited && edited != code
136
+ $stdout.puts colorize("# Edited code:", :yellow)
137
+ $stdout.puts highlight_code(edited)
138
+ $stdout.print colorize("Execute edited code? [y/N] ", :yellow)
139
+ edit_answer = $stdin.gets.to_s.strip.downcase
140
+ echo_stdin(edit_answer)
141
+ if edit_answer == 'y'
142
+ return execute(edited)
143
+ else
144
+ $stdout.puts colorize("Cancelled.", :yellow)
145
+ return nil
146
+ end
142
147
  else
143
- $stdout.puts colorize("Cancelled.", :yellow)
144
- nil
148
+ return execute(code)
145
149
  end
150
+ when 'n', 'no', ''
151
+ $stdout.puts colorize("Cancelled.", :yellow)
152
+ @last_cancelled = true
153
+ return nil
146
154
  else
147
- execute(code)
155
+ $stdout.print colorize("Execute? [y/N/edit] ", :yellow)
156
+ @on_prompt&.call
157
+ answer = $stdin.gets.to_s.strip.downcase
158
+ @last_answer = answer
159
+ echo_stdin(answer)
148
160
  end
149
- else
150
- $stdout.puts colorize("Cancelled.", :yellow)
151
- @last_cancelled = true
152
- nil
153
161
  end
154
162
  end
155
163
 
156
164
  private
157
165
 
166
+ MAX_DISPLAY_LINES = 10
167
+ MAX_DISPLAY_CHARS = 2000
168
+
169
+ def display_result(result)
170
+ full = "=> #{result.inspect}"
171
+ lines = full.lines
172
+ total_lines = lines.length
173
+ total_chars = full.length
174
+
175
+ if total_lines <= MAX_DISPLAY_LINES && total_chars <= MAX_DISPLAY_CHARS
176
+ $stdout.puts colorize(full, :green)
177
+ else
178
+ # Truncate by lines first, then by chars
179
+ truncated = lines.first(MAX_DISPLAY_LINES).join
180
+ truncated = truncated[0, MAX_DISPLAY_CHARS] if truncated.length > MAX_DISPLAY_CHARS
181
+ $stdout.puts colorize(truncated, :green)
182
+
183
+ omitted_lines = [total_lines - MAX_DISPLAY_LINES, 0].max
184
+ omitted_chars = [total_chars - truncated.length, 0].max
185
+ parts = []
186
+ parts << "#{omitted_lines} lines" if omitted_lines > 0
187
+ parts << "#{omitted_chars} chars" if omitted_chars > 0
188
+ $stdout.puts colorize(" (omitting #{parts.join(', ')})", :yellow)
189
+ end
190
+ end
191
+
158
192
  # Write stdin input to the capture IO only (avoids double-echo on terminal)
159
193
  def echo_stdin(text)
160
194
  $stdout.secondary.write("#{text}\n") if $stdout.respond_to?(:secondary)
@@ -241,6 +241,11 @@ module ConsoleAgent
241
241
  break if input.downcase == 'exit' || input.downcase == 'quit'
242
242
  next if input.empty?
243
243
 
244
+ if input == '?' || input == '/'
245
+ display_help
246
+ next
247
+ end
248
+
244
249
  if input == '/auto'
245
250
  ConsoleAgent.configuration.auto_execute = !ConsoleAgent.configuration.auto_execute
246
251
  mode = ConsoleAgent.configuration.auto_execute ? 'ON' : 'OFF'
@@ -265,6 +270,16 @@ module ConsoleAgent
265
270
  next
266
271
  end
267
272
 
273
+ if input == '/system'
274
+ @interactive_old_stdout.puts "\e[2m#{context}\e[0m"
275
+ next
276
+ end
277
+
278
+ if input == '/context'
279
+ display_conversation
280
+ next
281
+ end
282
+
268
283
  if input == '/cost'
269
284
  display_cost_summary
270
285
  next
@@ -384,18 +399,11 @@ module ConsoleAgent
384
399
  result, tool_messages = send_query(nil, conversation: @history)
385
400
  rescue Providers::ProviderError => e
386
401
  if e.message.include?("prompt is too long") && @history.length >= 6
387
- $stdout.puts "\e[33m Context limit reached. Auto-compacting history...\e[0m"
388
- compact_history
389
- begin
390
- result, tool_messages = send_query(nil, conversation: @history)
391
- rescue Providers::ProviderError => e2
392
- $stderr.puts "\e[31m Still too large after compaction: #{e2.message}\e[0m"
393
- return :error
394
- end
402
+ $stdout.puts "\e[33m Context limit reached. Run /compact to reduce context size, then try again.\e[0m"
395
403
  else
396
404
  $stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
397
- return :error
398
405
  end
406
+ return :error
399
407
  rescue Interrupt
400
408
  $stdout.puts "\n\e[33m Aborted.\e[0m"
401
409
  return :interrupted
@@ -564,18 +572,8 @@ module ConsoleAgent
564
572
  last_tool_names = []
565
573
 
566
574
  exhausted = false
567
- thinking_suggested = false
568
575
 
569
576
  max_rounds.times do |round|
570
- if round == 5 && !thinking_suggested && !on_thinking_model?
571
- thinking_suggested = true
572
- thinking_name = ConsoleAgent.configuration.resolved_thinking_model
573
- $stdout.puts "\e[33m This query is using many tool rounds. Switch to thinking model (#{thinking_name})? [y/N]\e[0m"
574
- answer = Readline.readline(" ", false).to_s.strip.downcase
575
- if answer == 'y'
576
- upgrade_to_thinking_model
577
- end
578
- end
579
577
  if round == 0
580
578
  $stdout.puts "\e[2m Thinking...\e[0m"
581
579
  else
@@ -593,17 +591,7 @@ module ConsoleAgent
593
591
  provider.chat_with_tools(messages, tools: tools, system_prompt: active_system_prompt)
594
592
  end
595
593
  rescue Providers::ProviderError => e
596
- if e.message.include?("prompt is too long") && messages.length >= 6
597
- $stdout.puts "\e[33m Context limit hit mid-session. Compacting messages...\e[0m"
598
- messages = compact_messages(messages)
599
- unless @_retried_compact
600
- @_retried_compact = true
601
- retry
602
- end
603
- end
604
594
  raise
605
- ensure
606
- @_retried_compact = nil
607
595
  end
608
596
  total_input += result.input_tokens || 0
609
597
  total_output += result.output_tokens || 0
@@ -990,10 +978,7 @@ module ConsoleAgent
990
978
  def warn_if_history_large
991
979
  chars = @history.sum { |m| m[:content].to_s.length }
992
980
 
993
- if chars > 120_000 && @history.length >= 6
994
- $stdout.puts "\e[33m Context growing large (~#{format_tokens(chars)} chars). Auto-compacting...\e[0m"
995
- compact_history
996
- elsif chars > 50_000 && !@compact_warned
981
+ if chars > 50_000 && !@compact_warned
997
982
  @compact_warned = true
998
983
  $stdout.puts "\e[33m Conversation is getting large (~#{format_tokens(chars)} chars). Consider running /compact to reduce context size.\e[0m"
999
984
  end
@@ -1008,6 +993,9 @@ module ConsoleAgent
1008
993
  before_chars = @history.sum { |m| m[:content].to_s.length }
1009
994
  before_count = @history.length
1010
995
 
996
+ # Extract successfully executed code before summarizing
997
+ executed_code = extract_executed_code(@history)
998
+
1011
999
  $stdout.puts "\e[2m Compacting #{before_count} messages (~#{format_tokens(before_chars)} chars)...\e[0m"
1012
1000
 
1013
1001
  system_prompt = <<~PROMPT
@@ -1018,8 +1006,8 @@ module ConsoleAgent
1018
1006
  - Key findings and data discovered (include specific values, IDs, record counts)
1019
1007
  - Current state: what worked, what failed, where things stand
1020
1008
  - Important variable names, model names, or table names referenced
1021
- - Any code that was executed and its results
1022
1009
 
1010
+ Do NOT include code that was executed — that will be preserved separately.
1023
1011
  Be concise but preserve all information that would be needed to continue the conversation naturally.
1024
1012
  Do NOT include any preamble — just output the summary directly.
1025
1013
  PROMPT
@@ -1037,32 +1025,128 @@ module ConsoleAgent
1037
1025
  return
1038
1026
  end
1039
1027
 
1040
- @history = [{ role: :user, content: "CONVERSATION SUMMARY (compacted):\n#{summary}" }]
1028
+ content = "CONVERSATION SUMMARY (compacted):\n#{summary}"
1029
+ unless executed_code.empty?
1030
+ content += "\n\nCODE EXECUTED THIS SESSION (preserved for continuation):\n#{executed_code}"
1031
+ end
1032
+
1033
+ @history = [{ role: :user, content: content }]
1041
1034
  @compact_warned = false
1042
1035
 
1043
1036
  after_chars = @history.first[:content].length
1044
1037
  $stdout.puts "\e[36m Compacted: #{before_count} messages -> 1 summary (~#{format_tokens(before_chars)} -> ~#{format_tokens(after_chars)} chars)\e[0m"
1045
1038
  summary.each_line { |line| $stdout.puts "\e[2m #{line.rstrip}\e[0m" }
1039
+ if !executed_code.empty?
1040
+ $stdout.puts "\e[2m (preserved #{executed_code.scan(/```ruby/).length} executed code block(s))\e[0m"
1041
+ end
1046
1042
  display_usage(result)
1047
1043
  rescue => e
1048
1044
  $stdout.puts "\e[31m Compaction failed: #{e.message}\e[0m"
1049
1045
  end
1050
1046
  end
1051
1047
 
1052
- def compact_messages(messages)
1053
- return messages if messages.length < 6
1048
+ # Extracts code blocks that were successfully executed from conversation history.
1049
+ # Looks for:
1050
+ # 1. Assistant messages with ```ruby blocks followed by "Code was executed." user messages
1051
+ # 2. execute_plan tool calls followed by results without ERROR
1052
+ # Skips code that failed or was declined.
1053
+ def extract_executed_code(history)
1054
+ code_blocks = []
1055
+ history.each_cons(2) do |msg, next_msg|
1056
+ # Pattern 1: Assistant ```ruby blocks with successful execution
1057
+ if msg[:role].to_s == 'assistant' && next_msg[:role].to_s == 'user'
1058
+ content = msg[:content].to_s
1059
+ next_content = next_msg[:content].to_s
1060
+
1061
+ if next_content.start_with?('Code was executed.')
1062
+ content.scan(/```ruby\s*\n(.*?)```/m).each do |match|
1063
+ code = match[0].strip
1064
+ next if code.empty?
1065
+ result_summary = next_content[0..200].gsub("\n", "\n# ")
1066
+ code_blocks << "```ruby\n#{code}\n```\n# #{result_summary}"
1067
+ end
1068
+ end
1069
+ end
1054
1070
 
1055
- to_summarize = messages[0...-4]
1056
- to_keep = messages[-4..]
1071
+ # Pattern 2: execute_plan tool calls in provider-formatted messages
1072
+ if msg[:role].to_s == 'assistant' && msg[:content].is_a?(Array)
1073
+ msg[:content].each do |block|
1074
+ next unless block.is_a?(Hash) && block['type'] == 'tool_use' && block['name'] == 'execute_plan'
1075
+ input = block['input'] || {}
1076
+ steps = input['steps'] || []
1077
+
1078
+ # Find the matching tool_result in subsequent messages
1079
+ tool_id = block['id']
1080
+ result_msg = find_tool_result(history, tool_id)
1081
+ next unless result_msg
1082
+
1083
+ result_text = result_msg.to_s
1084
+ # Extract only steps that succeeded (no ERROR in their result)
1085
+ steps.each_with_index do |step, i|
1086
+ step_num = i + 1
1087
+ # Check if this specific step had an error
1088
+ step_section = result_text[/Step #{step_num}\b.*?(?=Step #{step_num + 1}\b|\z)/m] || ''
1089
+ next if step_section.include?('ERROR:')
1090
+ next if step_section.include?('User declined')
1091
+
1092
+ code = step['code'].to_s.strip
1093
+ next if code.empty?
1094
+ desc = step['description'] || "Step #{step_num}"
1095
+ code_blocks << "```ruby\n# #{desc}\n#{code}\n```"
1096
+ end
1097
+ end
1098
+ end
1099
+ end
1100
+ code_blocks.join("\n\n")
1101
+ end
1057
1102
 
1058
- history_text = to_summarize.map { |m| "#{m[:role]}: #{m[:content].to_s[0..500]}" }.join("\n\n")
1103
+ def find_tool_result(history, tool_id)
1104
+ history.each do |msg|
1105
+ next unless msg[:content].is_a?(Array)
1106
+ msg[:content].each do |block|
1107
+ next unless block.is_a?(Hash)
1108
+ if block['type'] == 'tool_result' && block['tool_use_id'] == tool_id
1109
+ return block['content']
1110
+ end
1111
+ # OpenAI format
1112
+ if msg[:role].to_s == 'tool' && msg[:tool_call_id] == tool_id
1113
+ return msg[:content]
1114
+ end
1115
+ end
1116
+ end
1117
+ nil
1118
+ end
1059
1119
 
1060
- summary_result = provider.chat(
1061
- [{ role: :user, content: "Summarize this conversation context concisely, preserving key facts, IDs, and findings:\n\n#{history_text}" }],
1062
- system_prompt: "You are a conversation summarizer. Be concise but preserve all actionable information."
1063
- )
1120
+ def display_conversation
1121
+ if @history.empty?
1122
+ @interactive_old_stdout.puts "\e[2m (no conversation history yet)\e[0m"
1123
+ return
1124
+ end
1125
+
1126
+ @interactive_old_stdout.puts "\e[36m Conversation (#{@history.length} messages):\e[0m"
1127
+ @history.each_with_index do |msg, i|
1128
+ role = msg[:role].to_s
1129
+ content = msg[:content].to_s
1130
+ label = role == 'user' ? "\e[33m[user]\e[0m" : "\e[36m[assistant]\e[0m"
1131
+ @interactive_old_stdout.puts "#{label} #{content}"
1132
+ @interactive_old_stdout.puts if i < @history.length - 1
1133
+ end
1134
+ end
1064
1135
 
1065
- [{ role: :user, content: "CONTEXT SUMMARY:\n#{summary_result.text}" }] + to_keep
1136
+ def display_help
1137
+ auto = ConsoleAgent.configuration.auto_execute ? 'ON' : 'OFF'
1138
+ @interactive_old_stdout.puts "\e[36m Commands:\e[0m"
1139
+ @interactive_old_stdout.puts "\e[2m /auto Toggle auto-execute (currently #{auto}) (Shift-Tab)\e[0m"
1140
+ @interactive_old_stdout.puts "\e[2m /think Switch to thinking model\e[0m"
1141
+ @interactive_old_stdout.puts "\e[2m /compact Summarize conversation to reduce context\e[0m"
1142
+ @interactive_old_stdout.puts "\e[2m /usage Show session token totals\e[0m"
1143
+ @interactive_old_stdout.puts "\e[2m /cost Show cost estimate by model\e[0m"
1144
+ @interactive_old_stdout.puts "\e[2m /name <lbl> Name this session for easy resume\e[0m"
1145
+ @interactive_old_stdout.puts "\e[2m /context Show conversation history sent to the LLM\e[0m"
1146
+ @interactive_old_stdout.puts "\e[2m /system Show the system prompt\e[0m"
1147
+ @interactive_old_stdout.puts "\e[2m /debug Toggle debug mode\e[0m"
1148
+ @interactive_old_stdout.puts "\e[2m > code Execute Ruby directly (skip LLM)\e[0m"
1149
+ @interactive_old_stdout.puts "\e[2m exit/quit Leave interactive mode\e[0m"
1066
1150
  end
1067
1151
 
1068
1152
  def display_exit_info
@@ -1,3 +1,3 @@
1
1
  module ConsoleAgent
2
- VERSION = '0.8.0'.freeze
2
+ VERSION = '0.9.0'.freeze
3
3
  end
@@ -35,7 +35,7 @@ ConsoleAgent.configure do |config|
35
35
  # config.connection_class = Sharding::CentralizedModel
36
36
 
37
37
  # Admin UI credentials (mount ConsoleAgent::Engine => '/console_agent' in routes.rb)
38
- # When nil, no authentication is required (convenient for development)
38
+ # When nil, all requests are denied. Set credentials or use config.authenticate.
39
39
  # config.admin_username = 'admin'
40
40
  # config.admin_password = 'changeme'
41
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: console_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cortfr
@@ -86,6 +86,7 @@ executables: []
86
86
  extensions: []
87
87
  extra_rdoc_files: []
88
88
  files:
89
+ - CHANGELOG.md
89
90
  - LICENSE
90
91
  - README.md
91
92
  - app/controllers/console_agent/application_controller.rb