Class: Clacky::AgentConfig

Inherits:
Object
  • Object
show all
Defined in:
lib/clacky/agent_config.rb

Constant Summary collapse

CONFIG_DIR =
File.join(Dir.home, ".clacky")
CONFIG_FILE =
File.join(CONFIG_DIR, "config.yml")
CLAUDE_DEFAULT_MODEL =

Default model for ClaudeCode environment

"claude-sonnet-4-5"
PERMISSION_MODES =
[:auto_approve, :confirm_safes, :confirm_all].freeze
FALLBACK_COOLING_OFF_SECONDS =

How long to stay on the fallback model before probing the primary again.

30 * 60

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ AgentConfig

Returns a new instance of AgentConfig.



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/clacky/agent_config.rb', line 158

def initialize(options = {})
  @permission_mode = validate_permission_mode(options[:permission_mode])
  @max_tokens = options[:max_tokens] || 8192
  @verbose = options[:verbose] || false
  @enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
  # Enable prompt caching by default for cost savings
  @enable_prompt_caching = options[:enable_prompt_caching].nil? ? true : options[:enable_prompt_caching]

  # Models configuration
  @models = options[:models] || []
  @current_model_index = options[:current_model_index] || 0

  # Memory and skill evolution configuration
  @memory_update_enabled = options[:memory_update_enabled].nil? ? true : options[:memory_update_enabled]
  @skill_evolution = options[:skill_evolution] || {
    enabled: true,
    auto_create_threshold: 12,
    reflection_mode: "llm_analysis"
  }
end

Instance Attribute Details

#current_model_indexObject

Returns the value of attribute current_model_index.



153
154
155
# File 'lib/clacky/agent_config.rb', line 153

def current_model_index
  @current_model_index
end

#enable_compressionObject

Returns the value of attribute enable_compression.



153
154
155
# File 'lib/clacky/agent_config.rb', line 153

def enable_compression
  @enable_compression
end

#enable_prompt_cachingObject

Returns the value of attribute enable_prompt_caching.



153
154
155
# File 'lib/clacky/agent_config.rb', line 153

def enable_prompt_caching
  @enable_prompt_caching
end

#max_tokensObject

Returns the value of attribute max_tokens.



153
154
155
# File 'lib/clacky/agent_config.rb', line 153

def max_tokens
  @max_tokens
end

#memory_update_enabledObject

Returns the value of attribute memory_update_enabled.



153
154
155
# File 'lib/clacky/agent_config.rb', line 153

def memory_update_enabled
  @memory_update_enabled
end

#modelsObject

Returns the value of attribute models.



153
154
155
# File 'lib/clacky/agent_config.rb', line 153

def models
  @models
end

#permission_modeObject

Returns the value of attribute permission_mode.



153
154
155
# File 'lib/clacky/agent_config.rb', line 153

def permission_mode
  @permission_mode
end

#skill_evolutionObject

Returns the value of attribute skill_evolution.



153
154
155
# File 'lib/clacky/agent_config.rb', line 153

def skill_evolution
  @skill_evolution
end

#verboseObject

Returns the value of attribute verbose.



153
154
155
# File 'lib/clacky/agent_config.rb', line 153

def verbose
  @verbose
end

Class Method Details

.load(config_file = CONFIG_FILE) ⇒ Object

Load configuration from file



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/clacky/agent_config.rb', line 180

def self.load(config_file = CONFIG_FILE)
  # Load from config file first
  if File.exist?(config_file)
    data = YAML.load_file(config_file)
  else
    data = nil
  end

  # Parse models from config
  models = parse_models(data)

  # Priority: config file > CLACKY_XXX env vars > ClaudeCode env vars
  if models.empty?
    # Try CLACKY_XXX environment variables first
    if ClackyEnv.default_configured?
      models << ClackyEnv.default_model_config
    # ClaudeCode (Anthropic) environment variable support is disabled
    # elsif ClaudeCodeEnv.configured?
    #   models << {
    #     "type" => "default",
    #     "api_key" => ClaudeCodeEnv.api_key,
    #     "base_url" => ClaudeCodeEnv.base_url,
    #     "model" => CLAUDE_DEFAULT_MODEL,
    #     "anthropic_format" => true
    #   }
    end

    # Add CLACKY_LITE_XXX if configured (only when loading from env)
    if ClackyEnv.lite_configured?
      models << ClackyEnv.lite_model_config
    end
  else
    # Config file exists, but check if we need to add env-based models
    # Only add if no model with that type exists
    has_default = models.any? { |m| m["type"] == "default" }
    has_lite = models.any? { |m| m["type"] == "lite" }

    # Add CLACKY default if not in config and env is set
    if !has_default && ClackyEnv.default_configured?
      models << ClackyEnv.default_model_config
    end

    # Add CLACKY lite if not in config and env is set
    if !has_lite && ClackyEnv.lite_configured?
      models << ClackyEnv.lite_model_config
    end

    # Ensure at least one model has type: default
    # If no model has type: default, assign it to the first model
    unless models.any? { |m| m["type"] == "default" }
      models.first["type"] = "default" if models.any?
    end
  end

  # Auto-inject lite model from provider preset when:
  # 1. A default model exists
  # 2. No lite model is configured yet (neither in file nor env)
  # 3. The default model's provider has a known lite_model
  # The injected lite model is runtime-only (not persisted to config.yml)
  inject_provider_lite_model(models)

  # Find the index of the model marked as "default" (type: default)
  # Fall back to 0 if no model has type: default
  default_index = models.find_index { |m| m["type"] == "default" } || 0

  new(models: models, current_model_index: default_index)
end

Instance Method Details

#activate_fallback!(fallback_model_name) ⇒ Object

Switch to fallback model and start the cooling-off clock. Idempotent — calling again while already in :fallback_active renews the timestamp.

Parameters:

  • fallback_model_name (String)

    the fallback model to use



435
436
437
438
439
# File 'lib/clacky/agent_config.rb', line 435

def activate_fallback!(fallback_model_name)
  @fallback_state = :fallback_active
  @fallback_since = Time.now
  @fallback_model  = fallback_model_name
end

#add_model(model:, api_key:, base_url:, anthropic_format: false, type: nil) ⇒ Object

Add a new model configuration



386
387
388
389
390
391
392
393
394
# File 'lib/clacky/agent_config.rb', line 386

def add_model(model:, api_key:, base_url:, anthropic_format: false, type: nil)
  @models << {
    "api_key" => api_key,
    "base_url" => base_url,
    "model" => model,
    "anthropic_format" => anthropic_format,
    "type" => type
  }.compact
end

#anthropic_format?Boolean

Check if should use Anthropic format for current model

Returns:

  • (Boolean)


376
377
378
# File 'lib/clacky/agent_config.rb', line 376

def anthropic_format?
  current_model&.dig("anthropic_format") || false
end

#api_keyObject

Get API key for current model



343
344
345
# File 'lib/clacky/agent_config.rb', line 343

def api_key
  current_model&.dig("api_key")
end

#api_key=(value) ⇒ Object

Set API key for current model



348
349
350
351
# File 'lib/clacky/agent_config.rb', line 348

def api_key=(value)
  return unless current_model
  current_model["api_key"] = value
end

#base_urlObject

Get base URL for current model



354
355
356
# File 'lib/clacky/agent_config.rb', line 354

def base_url
  current_model&.dig("base_url")
end

#base_url=(value) ⇒ Object

Set base URL for current model



359
360
361
362
# File 'lib/clacky/agent_config.rb', line 359

def base_url=(value)
  return unless current_model
  current_model["base_url"] = value
end

#bedrock?Boolean

Check if current model uses Bedrock Converse API (ABSK key prefix or abs- model prefix)

Returns:

  • (Boolean)


381
382
383
# File 'lib/clacky/agent_config.rb', line 381

def bedrock?
  Clacky::MessageFormat::Bedrock.bedrock_api_key?(api_key.to_s, model_name.to_s)
end

#confirm_fallback_ok!Object

Called when a successful API response is received. If we were :probing (testing primary after cooling-off), this confirms the primary model is healthy again and resets everything. No-op in :primary_ok or :fallback_active states.



456
457
458
459
460
461
462
# File 'lib/clacky/agent_config.rb', line 456

def confirm_fallback_ok!
  return unless @fallback_state == :probing

  @fallback_state = nil
  @fallback_since = nil
  @fallback_model = nil
end

#current_modelObject

Get current model configuration Looks for type: default first, falls back to current_model_index



309
310
311
312
# File 'lib/clacky/agent_config.rb', line 309

def current_model
  return nil if @models.empty?
  @models[@current_model_index]
end

#deep_copyObject

Save configuration to file Deep copy — models array contains mutable Hashes, so a shallow dup would let the copy share the same Hash objects with the original, causing Settings changes to silently mutate already-running session configs. JSON round-trip is the cleanest approach since @models is pure JSON-able data.



282
283
284
285
286
# File 'lib/clacky/agent_config.rb', line 282

def deep_copy
  copy = dup
  copy.instance_variable_set(:@models, JSON.parse(JSON.generate(@models)))
  copy
end

#default_modelObject

Get the default model (type: default) Falls back to current_model for backward compatibility



404
405
406
# File 'lib/clacky/agent_config.rb', line 404

def default_model
  find_model_by_type("default") || current_model
end

#effective_model_nameObject

The effective model name to use for API calls.

  • :primary_ok / nil → configured model_name (primary)

  • :fallback_active → fallback model

  • :probing → configured model_name (trying primary silently)



479
480
481
482
483
484
485
486
487
# File 'lib/clacky/agent_config.rb', line 479

def effective_model_name
  case @fallback_state
  when :fallback_active
    @fallback_model || model_name
  else
    # :primary_ok (nil) and :probing both use the primary model
    model_name
  end
end

#fallback_active?Boolean

Returns true when a fallback model is currently being used (:fallback_active or :probing states).

Returns:

  • (Boolean)


466
467
468
# File 'lib/clacky/agent_config.rb', line 466

def fallback_active?
  @fallback_state == :fallback_active || @fallback_state == :probing
end

#fallback_model_for(model_name) ⇒ String?

Look up the fallback model name for the given model name. Uses the provider preset’s fallback_models table. Returns nil if no fallback is configured for this model.

Parameters:

  • model_name (String)

    the primary model name (e.g. “abs-claude-sonnet-4-6”)

Returns:

  • (String, nil)


422
423
424
425
426
427
428
429
430
# File 'lib/clacky/agent_config.rb', line 422

def fallback_model_for(model_name)
  m = current_model
  return nil unless m

  provider_id = Clacky::Providers.find_by_base_url(m["base_url"])
  return nil unless provider_id

  Clacky::Providers.fallback_model(provider_id, model_name)
end

#find_model_by_type(type) ⇒ Object

Find model by type (default or lite) Returns the model hash or nil if not found



398
399
400
# File 'lib/clacky/agent_config.rb', line 398

def find_model_by_type(type)
  @models.find { |m| m["type"] == type }
end

#get_model(index) ⇒ Object

Get model by index



315
316
317
# File 'lib/clacky/agent_config.rb', line 315

def get_model(index)
  @models[index]
end

#lite_modelObject

Get the lite model (type: lite) Returns nil if no lite model configured



410
411
412
# File 'lib/clacky/agent_config.rb', line 410

def lite_model
  find_model_by_type("lite")
end

#maybe_start_probingObject

Called at the start of every call_llm. If cooling-off has expired, transition from :fallback_active → :probing so the next request will silently test the primary model. No-op in any other state.



445
446
447
448
449
450
# File 'lib/clacky/agent_config.rb', line 445

def maybe_start_probing
  return unless @fallback_state == :fallback_active
  return unless @fallback_since && (Time.now - @fallback_since) >= FALLBACK_COOLING_OFF_SECONDS

  @fallback_state = :probing
end

#model_nameObject

Get model name for current model



365
366
367
# File 'lib/clacky/agent_config.rb', line 365

def model_name
  current_model&.dig("model")
end

#model_name=(value) ⇒ Object

Set model name for current model



370
371
372
373
# File 'lib/clacky/agent_config.rb', line 370

def model_name=(value)
  return unless current_model
  current_model["model"] = value
end

#model_namesObject

List all model names



338
339
340
# File 'lib/clacky/agent_config.rb', line 338

def model_names
  @models.map { |m| m["model"] }
end

#models_configured?Boolean

Check if any model is configured

Returns:

  • (Boolean)


304
305
306
# File 'lib/clacky/agent_config.rb', line 304

def models_configured?
  !@models.empty? && !current_model.nil?
end

#probing?Boolean

Returns true only when we are silently probing the primary model.

Returns:

  • (Boolean)


471
472
473
# File 'lib/clacky/agent_config.rb', line 471

def probing?
  @fallback_state == :probing
end

#remove_model(index) ⇒ Object

Remove a model by index Returns true if removed, false if index out of range or it’s the last model



527
528
529
530
531
532
533
534
535
536
537
538
539
540
# File 'lib/clacky/agent_config.rb', line 527

def remove_model(index)
  # Don't allow removing the last model
  return false if @models.length <= 1
  return false if index < 0 || index >= @models.length
  
  @models.delete_at(index)
  
  # Adjust current_model_index if necessary
  if @current_model_index >= @models.length
    @current_model_index = @models.length - 1
  end
  
  true
end

#save(config_file = CONFIG_FILE) ⇒ Object



288
289
290
291
292
293
# File 'lib/clacky/agent_config.rb', line 288

def save(config_file = CONFIG_FILE)
  config_dir = File.dirname(config_file)
  FileUtils.mkdir_p(config_dir)
  File.write(config_file, to_yaml)
  FileUtils.chmod(0o600, config_file)
end

#set_model_type(index, type) ⇒ Object

Set a model’s type (default or lite) Ensures only one model has each type Returns true if successful

Parameters:

  • index (Integer)

    the model index

  • type (String, nil)

    “default”, “lite”, or nil to remove type



505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
# File 'lib/clacky/agent_config.rb', line 505

def set_model_type(index, type)
  return false if index < 0 || index >= @models.length
  return false unless ["default", "lite", nil].include?(type)

  if type
    # Remove type from any other model that has it
    @models.each do |m|
      m.delete("type") if m["type"] == type
    end
    
    # Set type on target model
    @models[index]["type"] = type
  else
    # Remove type from target model
    @models[index].delete("type")
  end

  true
end

#switch_model(index) ⇒ Object

Switch to model by index Updates the type: default to the selected model Returns true if switched, false if index out of range



322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/clacky/agent_config.rb', line 322

def switch_model(index)
  return false if index < 0 || index >= @models.length
  
  # Remove type: default from all models
  @models.each { |m| m.delete("type") if m["type"] == "default" }
  
  # Set type: default on the selected model
  @models[index]["type"] = "default"
  
  # Update current_model_index for backward compatibility
  @current_model_index = index
  
  true
end

#to_yamlObject

Convert to YAML format (top-level array) Auto-injected lite models (auto_injected: true) are excluded from persistence —they are regenerated at load time from the provider preset.



298
299
300
301
# File 'lib/clacky/agent_config.rb', line 298

def to_yaml
  persistable = @models.reject { |m| m["auto_injected"] }
  YAML.dump(persistable)
end