console_agent 0.0.1 → 0.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.
@@ -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
@@ -0,0 +1,95 @@
1
+ module ConsoleAgent
2
+ module Tools
3
+ class ModelTools
4
+ def list_models
5
+ return "ActiveRecord is not available." unless defined?(ActiveRecord::Base)
6
+
7
+ eager_load_app!
8
+ models = find_models
9
+ return "No models found." if models.empty?
10
+
11
+ lines = models.map do |model|
12
+ assoc_names = model.reflect_on_all_associations.map { |a| a.name.to_s }
13
+ if assoc_names.empty?
14
+ model.name
15
+ else
16
+ "#{model.name} (#{assoc_names.join(', ')})"
17
+ end
18
+ end
19
+
20
+ lines.join("\n")
21
+ rescue => e
22
+ "Error listing models: #{e.message}"
23
+ end
24
+
25
+ def describe_model(model_name)
26
+ return "ActiveRecord is not available." unless defined?(ActiveRecord::Base)
27
+ return "Error: model_name is required." if model_name.nil? || model_name.strip.empty?
28
+
29
+ eager_load_app!
30
+ model_name = model_name.strip
31
+
32
+ model = find_models.detect { |m| m.name == model_name || m.name.underscore == model_name.underscore }
33
+ return "Model '#{model_name}' not found. Use list_models to see available models." unless model
34
+
35
+ result = "Model: #{model.name}\n"
36
+ result += "Table: #{model.table_name}\n"
37
+
38
+ assocs = model.reflect_on_all_associations.map { |a| "#{a.macro} :#{a.name}" }
39
+ unless assocs.empty?
40
+ result += "Associations:\n"
41
+ assocs.each { |a| result += " #{a}\n" }
42
+ end
43
+
44
+ begin
45
+ validators = model.validators.map { |v|
46
+ attrs = v.attributes.join(', ')
47
+ kind = v.class.name.split('::').last.sub('Validator', '').downcase
48
+ "#{kind} on #{attrs}"
49
+ }.uniq
50
+ unless validators.empty?
51
+ result += "Validations:\n"
52
+ validators.each { |v| result += " #{v}\n" }
53
+ end
54
+ rescue => e
55
+ # validations may not be accessible
56
+ end
57
+
58
+ # Scopes - detect via singleton methods that aren't inherited
59
+ begin
60
+ base = defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base
61
+ scope_candidates = (model.singleton_methods - base.singleton_methods)
62
+ .reject { |m| m.to_s.start_with?('_') || m.to_s.start_with?('find') }
63
+ .sort
64
+ .first(20)
65
+ unless scope_candidates.empty?
66
+ result += "Possible scopes/class methods:\n"
67
+ scope_candidates.each { |s| result += " #{s}\n" }
68
+ end
69
+ rescue => e
70
+ # ignore
71
+ end
72
+
73
+ result
74
+ rescue => e
75
+ "Error describing model '#{model_name}': #{e.message}"
76
+ end
77
+
78
+ private
79
+
80
+ def find_models
81
+ base_class = defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base
82
+ ObjectSpace.each_object(Class).select { |c|
83
+ c < base_class && !c.abstract_class? && c.name && !c.name.start_with?('HABTM_')
84
+ }.sort_by(&:name)
85
+ end
86
+
87
+ def eager_load_app!
88
+ return unless defined?(Rails) && Rails.respond_to?(:application)
89
+ Rails.application.eager_load! if Rails.application.respond_to?(:eager_load!)
90
+ rescue => e
91
+ # ignore
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,253 @@
1
+ require 'json'
2
+
3
+ module ConsoleAgent
4
+ module Tools
5
+ class Registry
6
+ attr_reader :definitions
7
+
8
+ # Tools that should never be cached (side effects or user interaction)
9
+ NO_CACHE = %w[ask_user save_memory delete_memory].freeze
10
+
11
+ def initialize
12
+ @definitions = []
13
+ @handlers = {}
14
+ @cache = {}
15
+ @last_cached = false
16
+ register_all
17
+ end
18
+
19
+ def last_cached?
20
+ @last_cached
21
+ end
22
+
23
+ def execute(tool_name, arguments = {})
24
+ handler = @handlers[tool_name]
25
+ unless handler
26
+ return "Error: unknown tool '#{tool_name}'"
27
+ end
28
+
29
+ args = if arguments.is_a?(String)
30
+ begin
31
+ JSON.parse(arguments)
32
+ rescue
33
+ {}
34
+ end
35
+ else
36
+ arguments || {}
37
+ end
38
+
39
+ unless NO_CACHE.include?(tool_name)
40
+ cache_key = [tool_name, args].hash
41
+ if @cache.key?(cache_key)
42
+ @last_cached = true
43
+ return @cache[cache_key]
44
+ end
45
+ end
46
+
47
+ @last_cached = false
48
+ result = handler.call(args)
49
+ @cache[[tool_name, args].hash] = result unless NO_CACHE.include?(tool_name)
50
+ result
51
+ rescue => e
52
+ "Error executing #{tool_name}: #{e.message}"
53
+ end
54
+
55
+ def to_anthropic_format
56
+ definitions.map do |d|
57
+ tool = {
58
+ 'name' => d[:name],
59
+ 'description' => d[:description],
60
+ 'input_schema' => d[:parameters]
61
+ }
62
+ tool
63
+ end
64
+ end
65
+
66
+ def to_openai_format
67
+ definitions.map do |d|
68
+ {
69
+ 'type' => 'function',
70
+ 'function' => {
71
+ 'name' => d[:name],
72
+ 'description' => d[:description],
73
+ 'parameters' => d[:parameters]
74
+ }
75
+ }
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def register_all
82
+ require 'console_agent/tools/schema_tools'
83
+ require 'console_agent/tools/model_tools'
84
+ require 'console_agent/tools/code_tools'
85
+
86
+ schema = SchemaTools.new
87
+ models = ModelTools.new
88
+ code = CodeTools.new
89
+
90
+ register(
91
+ name: 'list_tables',
92
+ description: 'List all database table names in this Rails app.',
93
+ parameters: { 'type' => 'object', 'properties' => {} },
94
+ handler: ->(_args) { schema.list_tables }
95
+ )
96
+
97
+ register(
98
+ name: 'describe_table',
99
+ description: 'Get column names and types for a specific database table.',
100
+ parameters: {
101
+ 'type' => 'object',
102
+ 'properties' => {
103
+ 'table_name' => { 'type' => 'string', 'description' => 'The database table name (e.g. "users")' }
104
+ },
105
+ 'required' => ['table_name']
106
+ },
107
+ handler: ->(args) { schema.describe_table(args['table_name']) }
108
+ )
109
+
110
+ register(
111
+ name: 'list_models',
112
+ description: 'List all ActiveRecord model names with their association names.',
113
+ parameters: { 'type' => 'object', 'properties' => {} },
114
+ handler: ->(_args) { models.list_models }
115
+ )
116
+
117
+ register(
118
+ name: 'describe_model',
119
+ description: 'Get detailed info about a specific model: associations, validations, table name.',
120
+ parameters: {
121
+ 'type' => 'object',
122
+ 'properties' => {
123
+ 'model_name' => { 'type' => 'string', 'description' => 'The model class name (e.g. "User")' }
124
+ },
125
+ 'required' => ['model_name']
126
+ },
127
+ handler: ->(args) { models.describe_model(args['model_name']) }
128
+ )
129
+
130
+ register(
131
+ name: 'list_files',
132
+ description: 'List Ruby files in a directory of this Rails app. Defaults to app/ directory.',
133
+ parameters: {
134
+ 'type' => 'object',
135
+ 'properties' => {
136
+ 'directory' => { 'type' => 'string', 'description' => 'Relative directory path (e.g. "app/models", "lib"). Defaults to "app".' }
137
+ }
138
+ },
139
+ handler: ->(args) { code.list_files(args['directory']) }
140
+ )
141
+
142
+ register(
143
+ name: 'read_file',
144
+ description: 'Read the contents of a file in this Rails app. Capped at 200 lines.',
145
+ parameters: {
146
+ 'type' => 'object',
147
+ 'properties' => {
148
+ 'path' => { 'type' => 'string', 'description' => 'Relative file path (e.g. "app/models/user.rb")' }
149
+ },
150
+ 'required' => ['path']
151
+ },
152
+ handler: ->(args) { code.read_file(args['path']) }
153
+ )
154
+
155
+ register(
156
+ name: 'search_code',
157
+ description: 'Search for a pattern in Ruby files. Returns matching lines with file paths.',
158
+ parameters: {
159
+ 'type' => 'object',
160
+ 'properties' => {
161
+ 'query' => { 'type' => 'string', 'description' => 'Search pattern (substring match)' },
162
+ 'directory' => { 'type' => 'string', 'description' => 'Relative directory to search in. Defaults to "app".' }
163
+ },
164
+ 'required' => ['query']
165
+ },
166
+ handler: ->(args) { code.search_code(args['query'], args['directory']) }
167
+ )
168
+
169
+ register(
170
+ name: 'ask_user',
171
+ description: 'Ask the console user a clarifying question. Use this when you need specific information to write accurate code (e.g. which user they are, which record to target, what value to use). Do NOT generate placeholder values like YOUR_USER_ID — ask instead.',
172
+ parameters: {
173
+ 'type' => 'object',
174
+ 'properties' => {
175
+ 'question' => { 'type' => 'string', 'description' => 'The question to ask the user' }
176
+ },
177
+ 'required' => ['question']
178
+ },
179
+ handler: ->(args) { ask_user(args['question']) }
180
+ )
181
+
182
+ register_memory_tools
183
+ end
184
+
185
+ def register_memory_tools
186
+ return unless ConsoleAgent.configuration.memories_enabled
187
+
188
+ require 'console_agent/tools/memory_tools'
189
+ memory = MemoryTools.new
190
+
191
+ register(
192
+ name: 'save_memory',
193
+ 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).',
194
+ parameters: {
195
+ 'type' => 'object',
196
+ 'properties' => {
197
+ 'name' => { 'type' => 'string', 'description' => 'Short name for this memory (e.g. "Sharding architecture")' },
198
+ 'description' => { 'type' => 'string', 'description' => 'Detailed description of what you learned' },
199
+ 'tags' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Optional tags (e.g. ["database", "sharding"])' }
200
+ },
201
+ 'required' => ['name', 'description']
202
+ },
203
+ handler: ->(args) {
204
+ memory.save_memory(name: args['name'], description: args['description'], tags: args['tags'] || [])
205
+ }
206
+ )
207
+
208
+ register(
209
+ name: 'delete_memory',
210
+ description: 'Delete a memory by name.',
211
+ parameters: {
212
+ 'type' => 'object',
213
+ 'properties' => {
214
+ 'name' => { 'type' => 'string', 'description' => 'The memory name to delete (e.g. "Sharding architecture")' }
215
+ },
216
+ 'required' => ['name']
217
+ },
218
+ handler: ->(args) { memory.delete_memory(name: args['name']) }
219
+ )
220
+
221
+ register(
222
+ name: 'recall_memories',
223
+ description: 'Search your saved memories about this codebase. Call with no args to list all, or pass a query/tag to filter.',
224
+ parameters: {
225
+ 'type' => 'object',
226
+ 'properties' => {
227
+ 'query' => { 'type' => 'string', 'description' => 'Search term to filter by name, description, or tags' },
228
+ 'tag' => { 'type' => 'string', 'description' => 'Filter by a specific tag' }
229
+ }
230
+ },
231
+ handler: ->(args) { memory.recall_memories(query: args['query'], tag: args['tag']) }
232
+ )
233
+ end
234
+
235
+ def ask_user(question)
236
+ $stdout.puts "\e[36m ? #{question}\e[0m"
237
+ $stdout.print "\e[36m > \e[0m"
238
+ answer = $stdin.gets
239
+ return '(no answer provided)' if answer.nil?
240
+ answer.strip.empty? ? '(no answer provided)' : answer.strip
241
+ end
242
+
243
+ def register(name:, description:, parameters:, handler:)
244
+ @definitions << {
245
+ name: name,
246
+ description: description,
247
+ parameters: parameters
248
+ }
249
+ @handlers[name] = handler
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,60 @@
1
+ module ConsoleAgent
2
+ module Tools
3
+ class SchemaTools
4
+ def list_tables
5
+ return "ActiveRecord is not connected." unless ar_connected?
6
+
7
+ tables = connection.tables.sort
8
+ tables.reject! { |t| t == 'schema_migrations' || t == 'ar_internal_metadata' }
9
+ return "No tables found." if tables.empty?
10
+
11
+ tables.join(", ")
12
+ rescue => e
13
+ "Error listing tables: #{e.message}"
14
+ end
15
+
16
+ def describe_table(table_name)
17
+ return "ActiveRecord is not connected." unless ar_connected?
18
+ return "Error: table_name is required." if table_name.nil? || table_name.strip.empty?
19
+
20
+ table_name = table_name.strip
21
+ unless connection.tables.include?(table_name)
22
+ return "Table '#{table_name}' not found. Use list_tables to see available tables."
23
+ end
24
+
25
+ cols = connection.columns(table_name).map do |c|
26
+ parts = ["#{c.name}:#{c.type}"]
27
+ parts << "nullable" if c.null
28
+ parts << "default=#{c.default}" unless c.default.nil?
29
+ parts.join(" ")
30
+ end
31
+
32
+ indexes = connection.indexes(table_name).map do |idx|
33
+ unique = idx.unique ? "UNIQUE " : ""
34
+ "#{unique}INDEX on (#{idx.columns.join(', ')})"
35
+ end
36
+
37
+ result = "Table: #{table_name}\n"
38
+ result += "Columns:\n"
39
+ cols.each { |c| result += " #{c}\n" }
40
+ unless indexes.empty?
41
+ result += "Indexes:\n"
42
+ indexes.each { |i| result += " #{i}\n" }
43
+ end
44
+ result
45
+ rescue => e
46
+ "Error describing table '#{table_name}': #{e.message}"
47
+ end
48
+
49
+ private
50
+
51
+ def ar_connected?
52
+ defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
53
+ end
54
+
55
+ def connection
56
+ ActiveRecord::Base.connection
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ module ConsoleAgent
2
+ VERSION = '0.2.0'.freeze
3
+ end
data/lib/console_agent.rb CHANGED
@@ -1 +1,76 @@
1
- module ConsoleAgent; VERSION = '0.0.1'; end
1
+ require 'console_agent/version'
2
+ require 'console_agent/configuration'
3
+
4
+ module ConsoleAgent
5
+ class << self
6
+ def configuration
7
+ @configuration ||= Configuration.new
8
+ end
9
+
10
+ def configure
11
+ yield(configuration) if block_given?
12
+ end
13
+
14
+ def reset_configuration!
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
33
+ end
34
+
35
+ def logger
36
+ @logger ||= if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
37
+ Rails.logger
38
+ else
39
+ require 'logger'
40
+ Logger.new($stderr, progname: 'ConsoleAgent')
41
+ end
42
+ end
43
+
44
+ def logger=(log)
45
+ @logger = log
46
+ end
47
+
48
+ def status
49
+ c = configuration
50
+ key = c.resolved_api_key
51
+ masked_key = if key.nil? || key.empty?
52
+ "\e[31m(not set)\e[0m"
53
+ else
54
+ key[0..6] + '...' + key[-4..-1]
55
+ end
56
+
57
+ lines = []
58
+ lines << "\e[36m[ConsoleAgent v#{VERSION}]\e[0m"
59
+ lines << " Provider: #{c.provider}"
60
+ lines << " Model: #{c.resolved_model}"
61
+ lines << " API key: #{masked_key}"
62
+ lines << " Context mode: #{c.context_mode}"
63
+ lines << " Max tokens: #{c.max_tokens}"
64
+ lines << " Temperature: #{c.temperature}"
65
+ lines << " Timeout: #{c.timeout}s"
66
+ lines << " Max tool rounds:#{c.max_tool_rounds}" if c.context_mode == :smart
67
+ lines << " Auto-execute: #{c.auto_execute}"
68
+ lines << " Memories: #{c.memories_enabled}"
69
+ lines << " Debug: #{c.debug}"
70
+ $stdout.puts lines.join("\n")
71
+ nil
72
+ end
73
+ end
74
+ end
75
+
76
+ require 'console_agent/railtie' if defined?(Rails::Railtie)
@@ -0,0 +1,26 @@
1
+ require 'rails/generators'
2
+
3
+ module ConsoleAgent
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('templates', __dir__)
7
+ desc 'Creates a ConsoleAgent initializer in config/initializers/'
8
+
9
+ def copy_initializer
10
+ template 'initializer.rb', 'config/initializers/console_agent.rb'
11
+ end
12
+
13
+ def show_readme
14
+ say ''
15
+ say 'ConsoleAgent installed!', :green
16
+ say ''
17
+ say 'Next steps:'
18
+ say ' 1. Set your API key: export ANTHROPIC_API_KEY=sk-...'
19
+ say ' 2. Edit config/initializers/console_agent.rb if needed'
20
+ say ' 3. Run: rails console'
21
+ say ' 4. Try: ai "show me all tables"'
22
+ say ''
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ ConsoleAgent.configure do |config|
2
+ # LLM provider: :anthropic or :openai
3
+ config.provider = :anthropic
4
+
5
+ # API key (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env var)
6
+ # config.api_key = 'sk-...'
7
+
8
+ # Model override (defaults: claude-opus-4-6 for Anthropic, gpt-5.3-codex for OpenAI)
9
+ # config.model = 'claude-opus-4-6'
10
+
11
+ # Max tokens for LLM response
12
+ config.max_tokens = 4096
13
+
14
+ # Temperature (0.0 - 1.0)
15
+ config.temperature = 0.2
16
+
17
+ # Auto-execute generated code without confirmation (use with caution!)
18
+ config.auto_execute = false
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)
26
+ config.max_tool_rounds = 10
27
+
28
+ # HTTP timeout in seconds
29
+ config.timeout = 30
30
+
31
+ # Debug mode: prints full API requests/responses and tool calls to stderr
32
+ # config.debug = true
33
+ end