console_agent 0.1.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.
@@ -0,0 +1,63 @@
1
+ require 'fileutils'
2
+ require 'console_agent/storage/base'
3
+
4
+ module ConsoleAgent
5
+ module Storage
6
+ class FileStorage < Base
7
+ attr_reader :root_path
8
+
9
+ def initialize(root_path = nil)
10
+ @root_path = root_path || default_root
11
+ end
12
+
13
+ def read(key)
14
+ path = full_path(key)
15
+ return nil unless File.exist?(path)
16
+ File.read(path)
17
+ end
18
+
19
+ def write(key, content)
20
+ path = full_path(key)
21
+ FileUtils.mkdir_p(File.dirname(path))
22
+ File.write(path, content)
23
+ true
24
+ rescue Errno::EACCES, Errno::EROFS, IOError => e
25
+ raise StorageError, "Cannot write #{key}: #{e.message}"
26
+ end
27
+
28
+ def list(pattern)
29
+ Dir.glob(File.join(@root_path, pattern)).sort.map do |path|
30
+ path.sub("#{@root_path}/", '')
31
+ end
32
+ end
33
+
34
+ def exists?(key)
35
+ File.exist?(full_path(key))
36
+ end
37
+
38
+ def delete(key)
39
+ path = full_path(key)
40
+ return false unless File.exist?(path)
41
+ File.delete(path)
42
+ true
43
+ rescue Errno::EACCES, Errno::EROFS, IOError => e
44
+ raise StorageError, "Cannot delete #{key}: #{e.message}"
45
+ end
46
+
47
+ private
48
+
49
+ def full_path(key)
50
+ sanitized = key.gsub('..', '').gsub(%r{\A/+}, '')
51
+ File.join(@root_path, sanitized)
52
+ end
53
+
54
+ def default_root
55
+ if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
56
+ File.join(Rails.root.to_s, '.console_agent')
57
+ else
58
+ File.join(Dir.pwd, '.console_agent')
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,136 @@
1
+ require 'yaml'
2
+
3
+ module ConsoleAgent
4
+ module Tools
5
+ class MemoryTools
6
+ MEMORIES_DIR = 'memories'
7
+
8
+ def initialize(storage = nil)
9
+ @storage = storage || ConsoleAgent.storage
10
+ end
11
+
12
+ def save_memory(name:, description:, tags: [])
13
+ key = memory_key(name)
14
+ existing = load_memory(key)
15
+
16
+ frontmatter = {
17
+ 'name' => name,
18
+ 'tags' => Array(tags).empty? && existing ? (existing['tags'] || []) : Array(tags),
19
+ 'created_at' => existing ? existing['created_at'] : Time.now.utc.iso8601
20
+ }
21
+ frontmatter['updated_at'] = Time.now.utc.iso8601 if existing
22
+
23
+ content = "---\n#{YAML.dump(frontmatter).sub("---\n", '').strip}\n---\n\n#{description}\n"
24
+ @storage.write(key, content)
25
+
26
+ path = @storage.respond_to?(:root_path) ? File.join(@storage.root_path, key) : key
27
+ if existing
28
+ "Memory updated: \"#{name}\" (#{path})"
29
+ else
30
+ "Memory saved: \"#{name}\" (#{path})"
31
+ end
32
+ rescue Storage::StorageError => e
33
+ "FAILED to save (#{e.message}). Add this manually to .console_agent/#{key}:\n" \
34
+ "---\nname: #{name}\ntags: #{Array(tags).inspect}\n---\n\n#{description}"
35
+ end
36
+
37
+ def delete_memory(name:)
38
+ key = memory_key(name)
39
+ unless @storage.exists?(key)
40
+ # Try to find by name match across all memory files
41
+ found_key = find_memory_key_by_name(name)
42
+ return "No memory found: \"#{name}\"" unless found_key
43
+ key = found_key
44
+ end
45
+
46
+ memory = load_memory(key)
47
+ @storage.delete(key)
48
+ "Memory deleted: \"#{memory ? memory['name'] : name}\""
49
+ rescue Storage::StorageError => e
50
+ "FAILED to delete memory (#{e.message})."
51
+ end
52
+
53
+ def recall_memories(query: nil, tag: nil)
54
+ memories = load_all_memories
55
+ return "No memories stored yet." if memories.empty?
56
+
57
+ results = memories
58
+ if tag && !tag.empty?
59
+ results = results.select { |m|
60
+ Array(m['tags']).any? { |t| t.downcase.include?(tag.downcase) }
61
+ }
62
+ end
63
+ if query && !query.empty?
64
+ q = query.downcase
65
+ results = results.select { |m|
66
+ m['name'].to_s.downcase.include?(q) ||
67
+ m['description'].to_s.downcase.include?(q) ||
68
+ Array(m['tags']).any? { |t| t.downcase.include?(q) }
69
+ }
70
+ end
71
+
72
+ return "No memories matching your search." if results.empty?
73
+
74
+ results.map { |m|
75
+ line = "**#{m['name']}**\n#{m['description']}"
76
+ line += "\nTags: #{m['tags'].join(', ')}" if m['tags'] && !m['tags'].empty?
77
+ line
78
+ }.join("\n\n")
79
+ end
80
+
81
+ def memory_summaries
82
+ memories = load_all_memories
83
+ return nil if memories.empty?
84
+
85
+ memories.map { |m|
86
+ tags = Array(m['tags'])
87
+ tag_str = tags.empty? ? '' : " [#{tags.join(', ')}]"
88
+ "- #{m['name']}#{tag_str}"
89
+ }
90
+ end
91
+
92
+ private
93
+
94
+ def memory_key(name)
95
+ slug = name.downcase.strip
96
+ .gsub(/[^a-z0-9\s-]/, '')
97
+ .gsub(/[\s]+/, '-')
98
+ .gsub(/-+/, '-')
99
+ .sub(/^-/, '').sub(/-$/, '')
100
+ "#{MEMORIES_DIR}/#{slug}.md"
101
+ end
102
+
103
+ def load_memory(key)
104
+ content = @storage.read(key)
105
+ return nil if content.nil? || content.strip.empty?
106
+ parse_memory(content)
107
+ rescue => e
108
+ ConsoleAgent.logger.warn("ConsoleAgent: failed to load memory #{key}: #{e.message}")
109
+ nil
110
+ end
111
+
112
+ def load_all_memories
113
+ keys = @storage.list("#{MEMORIES_DIR}/*.md")
114
+ keys.map { |key| load_memory(key) }.compact
115
+ rescue => e
116
+ ConsoleAgent.logger.warn("ConsoleAgent: failed to load memories: #{e.message}")
117
+ []
118
+ end
119
+
120
+ def parse_memory(content)
121
+ return nil unless content =~ /\A---\s*\n(.*?\n)---\s*\n(.*)/m
122
+ frontmatter = YAML.safe_load($1, permitted_classes: [Time, Date]) || {}
123
+ description = $2.strip
124
+ frontmatter.merge('description' => description)
125
+ end
126
+
127
+ def find_memory_key_by_name(name)
128
+ keys = @storage.list("#{MEMORIES_DIR}/*.md")
129
+ keys.find do |key|
130
+ memory = load_memory(key)
131
+ memory && memory['name'].to_s.downcase == name.downcase
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -5,12 +5,22 @@ module ConsoleAgent
5
5
  class Registry
6
6
  attr_reader :definitions
7
7
 
8
- def initialize
8
+ # Tools that should never be cached (side effects or user interaction)
9
+ NO_CACHE = %w[ask_user save_memory delete_memory execute_plan].freeze
10
+
11
+ def initialize(executor: nil)
12
+ @executor = executor
9
13
  @definitions = []
10
14
  @handlers = {}
15
+ @cache = {}
16
+ @last_cached = false
11
17
  register_all
12
18
  end
13
19
 
20
+ def last_cached?
21
+ @last_cached
22
+ end
23
+
14
24
  def execute(tool_name, arguments = {})
15
25
  handler = @handlers[tool_name]
16
26
  unless handler
@@ -27,7 +37,18 @@ module ConsoleAgent
27
37
  arguments || {}
28
38
  end
29
39
 
30
- handler.call(args)
40
+ unless NO_CACHE.include?(tool_name)
41
+ cache_key = [tool_name, args].hash
42
+ if @cache.key?(cache_key)
43
+ @last_cached = true
44
+ return @cache[cache_key]
45
+ end
46
+ end
47
+
48
+ @last_cached = false
49
+ result = handler.call(args)
50
+ @cache[[tool_name, args].hash] = result unless NO_CACHE.include?(tool_name)
51
+ result
31
52
  rescue => e
32
53
  "Error executing #{tool_name}: #{e.message}"
33
54
  end
@@ -158,6 +179,211 @@ module ConsoleAgent
158
179
  },
159
180
  handler: ->(args) { ask_user(args['question']) }
160
181
  )
182
+
183
+ register_memory_tools
184
+ register_execute_plan
185
+ end
186
+
187
+ def register_memory_tools
188
+ return unless ConsoleAgent.configuration.memories_enabled
189
+
190
+ require 'console_agent/tools/memory_tools'
191
+ memory = MemoryTools.new
192
+
193
+ register(
194
+ name: 'save_memory',
195
+ description: 'Save a fact or pattern you learned about this codebase for future sessions. Use after discovering how something works (e.g. sharding, auth, custom business logic).',
196
+ parameters: {
197
+ 'type' => 'object',
198
+ 'properties' => {
199
+ 'name' => { 'type' => 'string', 'description' => 'Short name for this memory (e.g. "Sharding architecture")' },
200
+ 'description' => { 'type' => 'string', 'description' => 'Detailed description of what you learned' },
201
+ 'tags' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Optional tags (e.g. ["database", "sharding"])' }
202
+ },
203
+ 'required' => ['name', 'description']
204
+ },
205
+ handler: ->(args) {
206
+ memory.save_memory(name: args['name'], description: args['description'], tags: args['tags'] || [])
207
+ }
208
+ )
209
+
210
+ register(
211
+ name: 'delete_memory',
212
+ description: 'Delete a memory by name.',
213
+ parameters: {
214
+ 'type' => 'object',
215
+ 'properties' => {
216
+ 'name' => { 'type' => 'string', 'description' => 'The memory name to delete (e.g. "Sharding architecture")' }
217
+ },
218
+ 'required' => ['name']
219
+ },
220
+ handler: ->(args) { memory.delete_memory(name: args['name']) }
221
+ )
222
+
223
+ register(
224
+ name: 'recall_memories',
225
+ description: 'Search your saved memories about this codebase. Call with no args to list all, or pass a query/tag to filter.',
226
+ parameters: {
227
+ 'type' => 'object',
228
+ 'properties' => {
229
+ 'query' => { 'type' => 'string', 'description' => 'Search term to filter by name, description, or tags' },
230
+ 'tag' => { 'type' => 'string', 'description' => 'Filter by a specific tag' }
231
+ }
232
+ },
233
+ handler: ->(args) { memory.recall_memories(query: args['query'], tag: args['tag']) }
234
+ )
235
+ end
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
161
387
  end
162
388
 
163
389
  def ask_user(question)
@@ -1,3 +1,3 @@
1
1
  module ConsoleAgent
2
- VERSION = '0.1.0'.freeze
2
+ VERSION = '0.3.0'.freeze
3
3
  end
data/lib/console_agent.rb CHANGED
@@ -13,6 +13,23 @@ module ConsoleAgent
13
13
 
14
14
  def reset_configuration!
15
15
  @configuration = Configuration.new
16
+ reset_storage!
17
+ end
18
+
19
+ def storage
20
+ @storage ||= begin
21
+ adapter = configuration.storage_adapter
22
+ if adapter
23
+ adapter
24
+ else
25
+ require 'console_agent/storage/file_storage'
26
+ Storage::FileStorage.new
27
+ end
28
+ end
29
+ end
30
+
31
+ def reset_storage!
32
+ @storage = nil
16
33
  end
17
34
 
18
35
  def logger
@@ -28,6 +45,14 @@ module ConsoleAgent
28
45
  @logger = log
29
46
  end
30
47
 
48
+ def current_user
49
+ @current_user
50
+ end
51
+
52
+ def current_user=(name)
53
+ @current_user = name
54
+ end
55
+
31
56
  def status
32
57
  c = configuration
33
58
  key = c.resolved_api_key
@@ -42,17 +67,132 @@ module ConsoleAgent
42
67
  lines << " Provider: #{c.provider}"
43
68
  lines << " Model: #{c.resolved_model}"
44
69
  lines << " API key: #{masked_key}"
45
- lines << " Context mode: #{c.context_mode}"
46
70
  lines << " Max tokens: #{c.max_tokens}"
47
71
  lines << " Temperature: #{c.temperature}"
48
72
  lines << " Timeout: #{c.timeout}s"
49
- lines << " Max tool rounds:#{c.max_tool_rounds}" if c.context_mode == :smart
73
+ lines << " Max tool rounds:#{c.max_tool_rounds}"
50
74
  lines << " Auto-execute: #{c.auto_execute}"
75
+ lines << " Memories: #{c.memories_enabled}"
76
+ lines << " Session logging:#{session_table_status}"
51
77
  lines << " Debug: #{c.debug}"
52
78
  $stdout.puts lines.join("\n")
53
79
  nil
54
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
55
192
  end
56
193
  end
57
194
 
58
- 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