console_agent 0.2.0 → 0.3.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.
@@ -6,9 +6,10 @@ module ConsoleAgent
6
6
  attr_reader :definitions
7
7
 
8
8
  # Tools that should never be cached (side effects or user interaction)
9
- NO_CACHE = %w[ask_user save_memory delete_memory].freeze
9
+ NO_CACHE = %w[ask_user save_memory delete_memory execute_plan].freeze
10
10
 
11
- def initialize
11
+ def initialize(executor: nil)
12
+ @executor = executor
12
13
  @definitions = []
13
14
  @handlers = {}
14
15
  @cache = {}
@@ -180,6 +181,7 @@ module ConsoleAgent
180
181
  )
181
182
 
182
183
  register_memory_tools
184
+ register_execute_plan
183
185
  end
184
186
 
185
187
  def register_memory_tools
@@ -232,6 +234,158 @@ module ConsoleAgent
232
234
  )
233
235
  end
234
236
 
237
+ def register_execute_plan
238
+ return unless @executor
239
+
240
+ register(
241
+ name: 'execute_plan',
242
+ description: 'Execute a multi-step plan. Each step has a description and Ruby code. The plan is shown to the user for approval, then each step is executed in order. After each step executes, its return value is stored as step1, step2, etc. Use these variables in later steps to reference earlier results (e.g. `token = step1`).',
243
+ parameters: {
244
+ 'type' => 'object',
245
+ 'properties' => {
246
+ 'steps' => {
247
+ 'type' => 'array',
248
+ 'description' => 'Ordered list of steps to execute',
249
+ 'items' => {
250
+ 'type' => 'object',
251
+ 'properties' => {
252
+ 'description' => { 'type' => 'string', 'description' => 'What this step does' },
253
+ 'code' => { 'type' => 'string', 'description' => 'Ruby code to execute' }
254
+ },
255
+ 'required' => %w[description code]
256
+ }
257
+ }
258
+ },
259
+ 'required' => ['steps']
260
+ },
261
+ handler: ->(args) { execute_plan(args['steps'] || []) }
262
+ )
263
+ end
264
+
265
+ def execute_plan(steps)
266
+ return 'No steps provided.' if steps.nil? || steps.empty?
267
+
268
+ auto = ConsoleAgent.configuration.auto_execute
269
+
270
+ # Display full plan
271
+ $stdout.puts
272
+ $stdout.puts "\e[36m Plan (#{steps.length} steps):\e[0m"
273
+ steps.each_with_index do |step, i|
274
+ $stdout.puts "\e[36m #{i + 1}. #{step['description']}\e[0m"
275
+ $stdout.puts highlight_plan_code(step['code'])
276
+ end
277
+ $stdout.puts
278
+
279
+ # Ask for plan approval (unless auto-execute)
280
+ skip_confirmations = auto
281
+ unless auto
282
+ $stdout.print "\e[33m Accept plan? [y/N/a(uto)] \e[0m"
283
+ answer = $stdin.gets.to_s.strip.downcase
284
+ case answer
285
+ when 'a', 'auto'
286
+ skip_confirmations = true
287
+ when 'y', 'yes'
288
+ # proceed with per-step confirmation
289
+ else
290
+ $stdout.puts "\e[33m Plan declined.\e[0m"
291
+ feedback = ask_feedback("What would you like changed?")
292
+ return "User declined the plan. Feedback: #{feedback}"
293
+ end
294
+ end
295
+
296
+ # Execute steps one by one
297
+ results = []
298
+ steps.each_with_index do |step, i|
299
+ $stdout.puts
300
+ $stdout.puts "\e[36m Step #{i + 1}/#{steps.length}: #{step['description']}\e[0m"
301
+ $stdout.puts "\e[33m # Code:\e[0m"
302
+ $stdout.puts highlight_plan_code(step['code'])
303
+
304
+ # Per-step confirmation (unless auto-execute or plan-level auto)
305
+ unless skip_confirmations
306
+ $stdout.print "\e[33m Run? [y/N/edit] \e[0m"
307
+ step_answer = $stdin.gets.to_s.strip.downcase
308
+
309
+ case step_answer
310
+ when 'e', 'edit'
311
+ edited = edit_step_code(step['code'])
312
+ if edited && edited != step['code']
313
+ $stdout.puts "\e[33m # Edited code:\e[0m"
314
+ $stdout.puts highlight_plan_code(edited)
315
+ $stdout.print "\e[33m Run edited code? [y/N] \e[0m"
316
+ confirm = $stdin.gets.to_s.strip.downcase
317
+ unless confirm == 'y' || confirm == 'yes'
318
+ feedback = ask_feedback("What would you like changed?")
319
+ results << "Step #{i + 1}: User declined after edit. Feedback: #{feedback}"
320
+ break
321
+ end
322
+ step['code'] = edited
323
+ end
324
+ when 'y', 'yes'
325
+ # proceed
326
+ else
327
+ feedback = ask_feedback("What would you like changed?")
328
+ results << "Step #{i + 1}: User declined. Feedback: #{feedback}"
329
+ break
330
+ end
331
+ end
332
+
333
+ exec_result = @executor.execute(step['code'])
334
+ # Make result available as step1, step2, etc. for subsequent steps
335
+ @executor.binding_context.local_variable_set(:"step#{i + 1}", exec_result)
336
+ output = @executor.last_output
337
+
338
+ step_report = "Step #{i + 1} (#{step['description']}):\n"
339
+ if output && !output.strip.empty?
340
+ step_report += "Output: #{output.strip}\n"
341
+ end
342
+ step_report += "Return value: #{exec_result.inspect}"
343
+ results << step_report
344
+ end
345
+
346
+ results.join("\n\n")
347
+ end
348
+
349
+ def highlight_plan_code(code)
350
+ if coderay_available?
351
+ CodeRay.scan(code, :ruby).terminal.gsub(/^/, ' ')
352
+ else
353
+ code.split("\n").map { |l| " \e[37m#{l}\e[0m" }.join("\n")
354
+ end
355
+ end
356
+
357
+ def edit_step_code(code)
358
+ require 'tempfile'
359
+ editor = ENV['EDITOR'] || 'vi'
360
+ tmpfile = Tempfile.new(['console_agent_step', '.rb'])
361
+ tmpfile.write(code)
362
+ tmpfile.flush
363
+ system("#{editor} #{tmpfile.path}")
364
+ File.read(tmpfile.path).strip
365
+ rescue => e
366
+ $stderr.puts "\e[31m Editor error: #{e.message}\e[0m"
367
+ code
368
+ ensure
369
+ tmpfile.close! if tmpfile
370
+ end
371
+
372
+ def coderay_available?
373
+ return @coderay_available unless @coderay_available.nil?
374
+ @coderay_available = begin
375
+ require 'coderay'
376
+ true
377
+ rescue LoadError
378
+ false
379
+ end
380
+ end
381
+
382
+ def ask_feedback(prompt)
383
+ $stdout.print "\e[36m #{prompt} > \e[0m"
384
+ feedback = $stdin.gets
385
+ return '(no feedback provided)' if feedback.nil?
386
+ feedback.strip.empty? ? '(no feedback provided)' : feedback.strip
387
+ end
388
+
235
389
  def ask_user(question)
236
390
  $stdout.puts "\e[36m ? #{question}\e[0m"
237
391
  $stdout.print "\e[36m > \e[0m"
@@ -1,3 +1,3 @@
1
1
  module ConsoleAgent
2
- VERSION = '0.2.0'.freeze
2
+ VERSION = '0.3.0'.freeze
3
3
  end
data/lib/console_agent.rb CHANGED
@@ -45,6 +45,14 @@ module ConsoleAgent
45
45
  @logger = log
46
46
  end
47
47
 
48
+ def current_user
49
+ @current_user
50
+ end
51
+
52
+ def current_user=(name)
53
+ @current_user = name
54
+ end
55
+
48
56
  def status
49
57
  c = configuration
50
58
  key = c.resolved_api_key
@@ -59,18 +67,132 @@ module ConsoleAgent
59
67
  lines << " Provider: #{c.provider}"
60
68
  lines << " Model: #{c.resolved_model}"
61
69
  lines << " API key: #{masked_key}"
62
- lines << " Context mode: #{c.context_mode}"
63
70
  lines << " Max tokens: #{c.max_tokens}"
64
71
  lines << " Temperature: #{c.temperature}"
65
72
  lines << " Timeout: #{c.timeout}s"
66
- lines << " Max tool rounds:#{c.max_tool_rounds}" if c.context_mode == :smart
73
+ lines << " Max tool rounds:#{c.max_tool_rounds}"
67
74
  lines << " Auto-execute: #{c.auto_execute}"
68
75
  lines << " Memories: #{c.memories_enabled}"
76
+ lines << " Session logging:#{session_table_status}"
69
77
  lines << " Debug: #{c.debug}"
70
78
  $stdout.puts lines.join("\n")
71
79
  nil
72
80
  end
81
+
82
+ def setup!
83
+ conn = session_connection
84
+ table = 'console_agent_sessions'
85
+
86
+ if conn.table_exists?(table)
87
+ $stdout.puts "\e[32mConsoleAgent: #{table} already exists. Run ConsoleAgent.teardown! first to recreate.\e[0m"
88
+ else
89
+ conn.create_table(table) do |t|
90
+ t.text :query, null: false
91
+ t.text :conversation, null: false
92
+ t.integer :input_tokens, default: 0
93
+ t.integer :output_tokens, default: 0
94
+ t.string :user_name, limit: 255
95
+ t.string :mode, limit: 20, null: false
96
+ t.text :code_executed
97
+ t.text :code_output
98
+ t.text :code_result
99
+ t.text :console_output
100
+ t.boolean :executed, default: false
101
+ t.string :provider, limit: 50
102
+ t.string :model, limit: 100
103
+ t.string :name, limit: 255
104
+ t.integer :duration_ms
105
+ t.datetime :created_at, null: false
106
+ end
107
+
108
+ conn.add_index(table, :created_at)
109
+ conn.add_index(table, :user_name)
110
+ conn.add_index(table, :name)
111
+
112
+ $stdout.puts "\e[32mConsoleAgent: created #{table} table.\e[0m"
113
+ end
114
+ rescue => e
115
+ $stderr.puts "\e[31mConsoleAgent setup failed: #{e.class}: #{e.message}\e[0m"
116
+ end
117
+
118
+ def migrate!
119
+ conn = session_connection
120
+ table = 'console_agent_sessions'
121
+
122
+ unless conn.table_exists?(table)
123
+ $stderr.puts "\e[33mConsoleAgent: #{table} does not exist. Run ConsoleAgent.setup! first.\e[0m"
124
+ return
125
+ end
126
+
127
+ migrations = []
128
+
129
+ unless conn.column_exists?(table, :name)
130
+ conn.add_column(table, :name, :string, limit: 255)
131
+ conn.add_index(table, :name) unless conn.index_exists?(table, :name)
132
+ migrations << 'name'
133
+ end
134
+
135
+ if migrations.empty?
136
+ $stdout.puts "\e[32mConsoleAgent: #{table} is up to date.\e[0m"
137
+ else
138
+ $stdout.puts "\e[32mConsoleAgent: added columns: #{migrations.join(', ')}.\e[0m"
139
+ end
140
+ rescue => e
141
+ $stderr.puts "\e[31mConsoleAgent migrate failed: #{e.class}: #{e.message}\e[0m"
142
+ end
143
+
144
+ def teardown!
145
+ conn = session_connection
146
+ table = 'console_agent_sessions'
147
+
148
+ unless conn.table_exists?(table)
149
+ $stdout.puts "\e[33mConsoleAgent: #{table} does not exist, nothing to remove.\e[0m"
150
+ return
151
+ end
152
+
153
+ count = conn.select_value("SELECT COUNT(*) FROM #{conn.quote_table_name(table)}")
154
+ $stdout.print "\e[33mDrop #{table} (#{count} sessions)? [y/N] \e[0m"
155
+ answer = $stdin.gets.to_s.strip.downcase
156
+
157
+ unless answer == 'y' || answer == 'yes'
158
+ $stdout.puts "\e[33mCancelled.\e[0m"
159
+ return
160
+ end
161
+
162
+ conn.drop_table(table)
163
+ $stdout.puts "\e[32mConsoleAgent: dropped #{table}.\e[0m"
164
+ rescue => e
165
+ $stderr.puts "\e[31mConsoleAgent teardown failed: #{e.class}: #{e.message}\e[0m"
166
+ end
167
+
168
+ private
169
+
170
+ def session_table_status
171
+ return 'disabled' unless configuration.session_logging
172
+ conn = session_connection
173
+ if conn.table_exists?('console_agent_sessions')
174
+ count = conn.select_value("SELECT COUNT(*) FROM #{conn.quote_table_name('console_agent_sessions')}")
175
+ "\e[32m#{count} sessions\e[0m"
176
+ else
177
+ "\e[33mtable missing (run ConsoleAgent.setup!)\e[0m"
178
+ end
179
+ rescue
180
+ "\e[33munavailable\e[0m"
181
+ end
182
+
183
+ def session_connection
184
+ klass = configuration.connection_class
185
+ if klass
186
+ klass = Object.const_get(klass) if klass.is_a?(String)
187
+ klass.connection
188
+ else
189
+ ActiveRecord::Base.connection
190
+ end
191
+ end
73
192
  end
74
193
  end
75
194
 
76
- require 'console_agent/railtie' if defined?(Rails::Railtie)
195
+ if defined?(Rails::Railtie)
196
+ require 'console_agent/railtie'
197
+ require 'console_agent/engine'
198
+ end
@@ -17,12 +17,7 @@ ConsoleAgent.configure do |config|
17
17
  # Auto-execute generated code without confirmation (use with caution!)
18
18
  config.auto_execute = false
19
19
 
20
- # Context mode:
21
- # :smart - (default) minimal system prompt, LLM uses tools to fetch schema/model/code details on demand
22
- # :full - sends all schema, models, and routes in the system prompt every time
23
- config.context_mode = :smart
24
-
25
- # Max tool-use rounds per query in :smart mode (safety cap)
20
+ # Max tool-use rounds per query (safety cap)
26
21
  config.max_tool_rounds = 10
27
22
 
28
23
  # HTTP timeout in seconds
@@ -30,4 +25,17 @@ ConsoleAgent.configure do |config|
30
25
 
31
26
  # Debug mode: prints full API requests/responses and tool calls to stderr
32
27
  # config.debug = true
28
+
29
+ # Session logging: persist AI sessions to the database
30
+ # Run ConsoleAgent.setup! in the Rails console to create the table
31
+ config.session_logging = true
32
+
33
+ # Database connection for ConsoleAgent tables (default: ActiveRecord::Base)
34
+ # Set to a class that responds to .connection if tables live on a different DB
35
+ # config.connection_class = Sharding::CentralizedModel
36
+
37
+ # Admin UI credentials (mount ConsoleAgent::Engine => '/console_agent' in routes.rb)
38
+ # When nil, no authentication is required (convenient for development)
39
+ # config.admin_username = 'admin'
40
+ # config.admin_password = 'changeme'
33
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cortfr
@@ -88,16 +88,26 @@ extra_rdoc_files: []
88
88
  files:
89
89
  - LICENSE
90
90
  - README.md
91
+ - app/controllers/console_agent/application_controller.rb
92
+ - app/controllers/console_agent/sessions_controller.rb
93
+ - app/helpers/console_agent/sessions_helper.rb
94
+ - app/models/console_agent/session.rb
95
+ - app/views/console_agent/sessions/index.html.erb
96
+ - app/views/console_agent/sessions/show.html.erb
97
+ - app/views/layouts/console_agent/application.html.erb
98
+ - config/routes.rb
91
99
  - lib/console_agent.rb
92
100
  - lib/console_agent/configuration.rb
93
101
  - lib/console_agent/console_methods.rb
94
102
  - lib/console_agent/context_builder.rb
103
+ - lib/console_agent/engine.rb
95
104
  - lib/console_agent/executor.rb
96
105
  - lib/console_agent/providers/anthropic.rb
97
106
  - lib/console_agent/providers/base.rb
98
107
  - lib/console_agent/providers/openai.rb
99
108
  - lib/console_agent/railtie.rb
100
109
  - lib/console_agent/repl.rb
110
+ - lib/console_agent/session_logger.rb
101
111
  - lib/console_agent/storage/base.rb
102
112
  - lib/console_agent/storage/file_storage.rb
103
113
  - lib/console_agent/tools/code_tools.rb