Class: LittleGhost::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/little_ghost/configuration.rb

Overview

Configure shared services and lookup rules before agents start. A configuration collects model profiles, persistence, paths, instrumentation, and runtime hooks for an application.

LittleGhost.configure do |config|
config.default_model :customer_support
config.service_name "support-api"
end

LittleGhost.configuration.default_model # => "customer_support"
LittleGhost.configuration.service_name  # => "support-api"

Prompt and skill lookup paths default to app/prompts and app/skills under the application root. Applications may append shared roots or replace the arrays entirely.

Configuration is a mutable application builder until its shared Runtime is first used. A successful #runtime call locks the builder so standalone Agents and Assemblies keep one stable setup. Configure the application before its first entrypoint call. Explicit Runtime construction remains an advanced way to take an independent snapshot without selecting the shared default.

Session actor resolvers belong at an authentication boundary. Multi-tenant applications should derive actor identity from trusted authenticated state, not from an unverified request field.

Constant Summary collapse

FILE_LOAD_MUTEX =

:nodoc:

Mutex.new
CONFIGURATION_KEYS =

:nodoc:

%i[invocation service_name].freeze
DEFAULT_PROMPT_PATHS =

:nodoc:

["app/prompts"].freeze
DEFAULT_SKILL_PATHS =

:nodoc:

["app/skills"].freeze

Instance Method Summary collapse

Constructor Details

#initialize(values = {}) ⇒ Configuration

Starts a mutable builder with optional values.

Prompt paths default to app/prompts and skill paths to app/skills. Collection settings are copied so callers can safely reuse their input arrays after construction.



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/little_ghost/configuration.rb', line 89

def initialize(values = {})
  @lifecycle_monitor = Monitor.new
  @runtime_condition = @lifecycle_monitor.new_cond
  @configuration_values = {
    prompt_paths: DEFAULT_PROMPT_PATHS.dup,
    skill_paths: DEFAULT_SKILL_PATHS.dup,
    skill_resource_root: nil,
    workspace: nil,
    sandbox: nil,
    instrumentation_subscribers: [],
    runtime_hooks: []
  }.merge(values)
  @configuration_values[:prompt_paths] = Array(@configuration_values[:prompt_paths]).dup
  @configuration_values[:skill_paths] = Array(@configuration_values[:skill_paths]).dup
  @configuration_values[:instrumentation_subscribers] = Array(
    @configuration_values[:instrumentation_subscribers]
  ).dup
  @configuration_values[:runtime_hooks] = Array(@configuration_values[:runtime_hooks]).dup
  @configuration_values[:provider_adapters] = @configuration_values.fetch(:provider_adapters, {}).dup
  @configuration_values[:catalog_sources] = Array(@configuration_values[:catalog_sources]).dup
  @configuration_values[:provider_credentials] ||= nil
end

Instance Method Details

#[](name) ⇒ Object

Looks up an arbitrary setting by symbol or string-compatible name.



388
389
390
# File 'lib/little_ghost/configuration.rb', line 388

def [](name)
  configuration_values.fetch(name.to_sym)
end

#[]=(name, value) ⇒ Object

Adds or replaces an arbitrary setting.



393
394
395
# File 'lib/little_ghost/configuration.rb', line 393

def []=(name, value)
  change_configuration { configuration_values[name.to_sym] = value }
end

#catalog_source(source) ⇒ Object

Adds an explicit catalog source. Sources refresh only when callers invoke ModelResolver#refresh!.

Raises:

  • (ArgumentError)


329
330
331
332
333
334
335
336
337
338
# File 'lib/little_ghost/configuration.rb', line 329

def catalog_source(source)
  ensure_configuration_open!
  raise ArgumentError, "catalog source must be a Models::Catalog::Source" unless source.is_a?(Models::Catalog::Source)

  change_configuration do
    configuration_values[:catalog_sources] << source
    @resolved_model_resolver = nil
  end
  source
end

#configureObject

Yields this builder for setup and returns the same instance.



113
114
115
116
# File 'lib/little_ghost/configuration.rb', line 113

def configure
  change_configuration { yield self } if block_given?
  self
end

#default_model(value = :__read__) ⇒ Object

Fallback logical role for the default resolver.



229
230
231
232
233
234
235
236
237
# File 'lib/little_ghost/configuration.rb', line 229

def default_model(value = :__read__)
  return configuration_values[:default_model] if value == :__read__

  change_configuration do
    configuration_values[:default_model] = value.to_s
    reset_model_resolver
  end
  value.to_s
end

#default_model=(value) ⇒ Object

Replaces the fallback logical role and normalizes it to a String.



240
241
242
# File 'lib/little_ghost/configuration.rb', line 240

def default_model=(value)
  default_model(value)
end

#instrument(subscriber) ⇒ Object

Adds an Instrumentation::Subscriber to each new runtime and returns it.



403
404
405
406
407
408
409
410
411
# File 'lib/little_ghost/configuration.rb', line 403

def instrument(subscriber)
  ensure_configuration_open!
  unless subscriber.is_a?(Instrumentation::Subscriber)
    raise ArgumentError, "instrumentation subscriber must be a LittleGhost::Instrumentation::Subscriber"
  end

  change_configuration { configuration_values[:instrumentation_subscribers] << subscriber }
  subscriber
end

#load_file!(root: nil) ⇒ Object

:nodoc:



522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/little_ghost/configuration.rb', line 522

def load_file!(root: nil) # :nodoc:
  requested_root = canonical_root(root || self.root)
  FILE_LOAD_MUTEX.synchronize do
    (@configuration_file_mutex ||= Mutex.new).synchronize do
      if @configuration_file_root
        return self if @configuration_file_root == requested_root

        raise ConfigurationError, "configuration file is already loaded for #{@configuration_file_root}"
      end

      path = File.join(requested_root, "config/little_ghost.rb")
      load_configuration_file(path) if File.file?(path)
      @configuration_file_root = requested_root
    end
  end

  self
end

#log_events_to(destination = :__read__) ⇒ Object

:call-seq:

log_events_to() -> :stdout, :stderr, nil
log_events_to(destination) -> destination

Sends structured framework events to :stdout or :stderr. This setting controls the process-wide Events console destination; the most recent setting replaces it without changing other event listeners. By default, events have no console destination. Passing nil disables console output. The console listener redacts sensitive values and writes one JSON object per line.



423
424
425
426
427
# File 'lib/little_ghost/configuration.rb', line 423

def log_events_to(destination = :__read__)
  return Events.console_output if destination == :__read__

  change_configuration { Events.console_output = destination }
end

#log_events_to=(destination) ⇒ Object

Replaces the console destination for structured framework events.



430
431
432
# File 'lib/little_ghost/configuration.rb', line 430

def log_events_to=(destination)
  log_events_to(destination)
end

#model_resolver(value = :__read__) ⇒ Object

Installs a complete resolver override for subsequently built runtimes.



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/little_ghost/configuration.rb', line 263

def model_resolver(value = :__read__)
  if value != :__read__
    ensure_configuration_open!
    validate_model_resolver_class!(value)

    change_configuration do
      configuration_values[:model_resolver] = value
      @resolved_model_resolver = nil
    end
    return value
  end

  @model_resolver_mutex ||= Mutex.new
  @model_resolver_mutex.synchronize do
    @resolved_model_resolver ||= begin
      providers = resolved_providers
      credential_resolver = configuration_values[:provider_credentials] || providers&.method(:credentials)
      configured = configuration_values[:model_resolver]
      if configured
        validate_model_resolver_configuration!
        configured.new(
          providers:,
          provider_adapters: configuration_values[:provider_adapters],
          catalog_sources: configuration_values[:catalog_sources],
          credential_resolver:
        )
      else
        profiles, file_default = resolved_models
        ModelResolver.new(
          providers:,
          profiles:,
          default_model: configuration_values.fetch(:default_model, file_default),
          provider_adapters: configuration_values[:provider_adapters],
          catalog_sources: configuration_values[:catalog_sources],
          credential_resolver:
        )
      end
    end
  end
end

#model_resolver=(value) ⇒ Object



304
305
306
# File 'lib/little_ghost/configuration.rb', line 304

def model_resolver=(value)
  model_resolver(value)
end

#models(value = :__read__) ⇒ Object

Logical model profiles for the default resolver. Role names cannot contain a colon because that syntax identifies a canonical model target.

Raises:

  • (ArgumentError)


211
212
213
214
215
216
217
218
219
220
221
# File 'lib/little_ghost/configuration.rb', line 211

def models(value = :__read__)
  return configuration_values[:models] if value == :__read__
  ensure_configuration_open!
  raise ArgumentError, "models must be a Hash" unless value.is_a?(Hash)

  change_configuration do
    configuration_values[:models] = value
    reset_model_resolver
  end
  value
end

#models=(value) ⇒ Object

Replaces logical model profiles for subsequently built runtimes.



224
225
226
# File 'lib/little_ghost/configuration.rb', line 224

def models=(value)
  models(value)
end

#models_path(value = :__read__) ⇒ Object

Model YAML path. The conventional path is optional; an explicitly set path must exist when a runtime is built.



255
# File 'lib/little_ghost/configuration.rb', line 255

def models_path(value = :__read__) = configuration_path(:models_path, "models.yml", value)

#models_path=(value) ⇒ Object

Replaces the model YAML path for subsequently built runtimes.



258
259
260
# File 'lib/little_ghost/configuration.rb', line 258

def models_path=(value)
  models_path(value)
end

#nameObject

Replaces the service name attached to telemetry from new runtimes. :method: service_name= :call-seq:

service_name=(value) -> value


54
55
56
57
58
59
60
# File 'lib/little_ghost/configuration.rb', line 54

CONFIGURATION_KEYS.each do |name|
  define_method(name) do |value = :__read__|
    return configuration_values[name] if value == :__read__

    change_configuration { configuration_values[name] = value }
  end
end

#prompt_pathsObject

Prompt lookup paths in precedence order. The Array is mutable until the shared Runtime is built.



485
# File 'lib/little_ghost/configuration.rb', line 485

def prompt_paths = configuration_values[:prompt_paths]

#prompt_paths=(value) ⇒ Object

Replaces prompt lookup paths with value converted to an Array.



488
489
490
# File 'lib/little_ghost/configuration.rb', line 488

def prompt_paths=(value)
  change_configuration { configuration_values[:prompt_paths] = Array(value) }
end

#provider_adapter(name, callable = nil, &factory) ⇒ Object

Registers a provider adapter factory under name.



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/little_ghost/configuration.rb', line 309

def provider_adapter(name, callable = nil, &factory)
  ensure_configuration_open!
  implementation = factory || callable
  if implementation.is_a?(Class)
    unless implementation <= Providers::Base
      raise ArgumentError, "provider adapter class must inherit LittleGhost::Providers::Base"
    end
  elsif !implementation.respond_to?(:call)
    raise ArgumentError, "provider adapter must be a Providers::Base class or callable factory"
  end

  change_configuration do
    configuration_values[:provider_adapters][name.to_s] = implementation
    @resolved_model_resolver = nil
  end
  implementation
end

#provider_credentials(callable = nil, &resolver) ⇒ Object

Installs a trusted callable that returns credential options for a named provider connection when each executable model is constructed.

Raises:

  • (ArgumentError)


342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/little_ghost/configuration.rb', line 342

def provider_credentials(callable = nil, &resolver)
  value = resolver || callable
  return configuration_values[:provider_credentials] unless value
  ensure_configuration_open!
  raise ArgumentError, "provider credential resolver must be callable" unless value.respond_to?(:call)

  change_configuration do
    configuration_values[:provider_credentials] = value
    @resolved_model_resolver = nil
  end
  value
end

#providers(value = :__read__) ⇒ Object

Trusted provider connections for the default or custom resolver.



189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/little_ghost/configuration.rb', line 189

def providers(value = :__read__)
  return configuration_values[:providers] if value == :__read__

  ensure_configuration_open!
  unless value.is_a?(Hash) || value.is_a?(Providers::Configuration)
    raise ArgumentError, "providers must be a Hash or LittleGhost::Providers::Configuration"
  end

  change_configuration do
    configuration_values[:providers] = value
    reset_model_resolver
  end
  value
end

#providers=(value) ⇒ Object

Replaces trusted provider connections for subsequently built runtimes.



205
206
207
# File 'lib/little_ghost/configuration.rb', line 205

def providers=(value)
  providers(value)
end

#providers_path(value = :__read__) ⇒ Object

Provider YAML path. The conventional path is optional; an explicitly set path must exist when a runtime is built.



246
# File 'lib/little_ghost/configuration.rb', line 246

def providers_path(value = :__read__) = configuration_path(:providers_path, "providers.yml", value)

#providers_path=(value) ⇒ Object

Replaces the provider YAML path for subsequently built runtimes.



249
250
251
# File 'lib/little_ghost/configuration.rb', line 249

def providers_path=(value)
  providers_path(value)
end

#root(value = :__read__) ⇒ Object

:call-seq:

root() -> Pathname
root(path) -> Pathname

The resolved application root, defaulting to Dir.pwd.

Setting or reading an invalid root raises ConfigurationError. Symlinks are resolved so runtimes and lookup paths share one stable boundary.



474
475
476
477
478
479
480
481
# File 'lib/little_ghost/configuration.rb', line 474

def root(value = :__read__)
  if value != :__read__
    return change_configuration { configuration_values[:root] = canonical_root(value) }
  end

  configured = configuration_values[:root]
  configured ? canonical_root(configured) : inferred_root
end

#root=(value) ⇒ Object

Replaces the application root after resolving it to a stable real path.



398
399
400
# File 'lib/little_ghost/configuration.rb', line 398

def root=(value)
  root(value)
end

#runtimeObject

Returns the shared Runtime for this configuration, building it on first use. Once construction succeeds, the configuration is locked so every standalone entrypoint continues to use one stable application setup. The conventional configuration file may finish loading during construction; other writes are rejected. A failed build leaves the configuration editable for a later attempt.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/little_ghost/configuration.rb', line 124

def runtime
  build_generation = @lifecycle_monitor.synchronize do
    loop do
      return @default_runtime if @default_runtime
      if @runtime_building
        if @runtime_builder_thread.equal?(Thread.current)
          raise ConfigurationError, "LittleGhost.runtime cannot be called while the shared Runtime is starting"
        end

        waiting_generation = @runtime_generation
        @runtime_condition.wait
        return @default_runtime if @default_runtime
        if @runtime_failure_generation == waiting_generation
          raise @runtime_failure
        end
      else
        sealed_values = freeze_configuration_copy(@configuration_values)
        @runtime_generation = @runtime_generation.to_i + 1
        @runtime_building = true
        @runtime_builder_thread = Thread.current
        @configuration_values = sealed_values
        break @runtime_generation
      end
    end
  end

  begin
    runtime_root = root
    load_file!(root: runtime_root)
    runtime_settings = settings(root: runtime_root)
    built = Runtime.new(configuration: self, settings: runtime_settings)
    @lifecycle_monitor.synchronize do
      configuration_values.freeze
      @default_runtime = built
    end
    built
  rescue => error
    editable_values = if @configuration_values.frozen?
      copy_configuration_value(@configuration_values)
    else
      @configuration_values
    end
    @lifecycle_monitor.synchronize do
      @configuration_values = editable_values
      @runtime_failure = error
      @runtime_failure_generation = build_generation
    end
    raise
  ensure
    @lifecycle_monitor.synchronize do
      @runtime_building = false
      @runtime_builder_thread = nil
      @runtime_condition.broadcast
    end
  end
end

#runtime_hook(hook_class) ⇒ Object

Adds a Runtime::Hook subclass to each new runtime and returns it.



435
436
437
438
439
440
441
442
443
# File 'lib/little_ghost/configuration.rb', line 435

def runtime_hook(hook_class)
  ensure_configuration_open!
  unless hook_class.is_a?(Class) && hook_class <= Runtime::Hook
    raise ArgumentError, "runtime_hook must be a LittleGhost::Runtime::Hook class"
  end

  change_configuration { configuration_values[:runtime_hooks] << hook_class }
  hook_class
end

#sandboxObject

Sandbox declaration used for subsequently built runtimes.



184
185
# File 'lib/little_ghost/configuration.rb', line 184

def sandbox = configuration_values[:sandbox]
# Session-store declaration used for subsequently built runtimes.

#sandbox=(value) ⇒ Object

Selects the Sandbox subclass instantiated around each run's workspace.



363
364
365
366
367
# File 'lib/little_ghost/configuration.rb', line 363

def sandbox=(value)
  change_configuration do
    @configuration_values[:sandbox] = component_class(value, Sandbox, :sandbox)
  end
end

#session_actor(value = :__read__, &resolver) ⇒ Object

:call-seq:

session_actor() -> callable, nil
session_actor(callable) -> callable
session_actor { |invocation| ... } -> callable

The callable that derives the persistence actor for each invocation.

Pass either a callable or a block. The configured resolver should use trusted authenticated identity in multi-tenant applications.

Raises:

  • (ArgumentError)


454
455
456
457
458
459
460
461
462
463
464
# File 'lib/little_ghost/configuration.rb', line 454

def session_actor(value = :__read__, &resolver)
  return configuration_values[:session_actor] if value == :__read__ && !resolver

  ensure_configuration_open!
  raise ArgumentError, "Provide a session actor resolver or a block, not both" if value != :__read__ && resolver

  configured = resolver || value
  raise ArgumentError, "session_actor must be callable" unless configured.respond_to?(:call)

  change_configuration { configuration_values[:session_actor] = configured }
end

#session_storeObject

Session-store declaration used for subsequently built runtimes.



186
# File 'lib/little_ghost/configuration.rb', line 186

def session_store = configuration_values[:session_store]

#session_store=(value) ⇒ Object

Selects session persistence with a :provider and its constructor options.

The provider must be a SessionStore subclass. Runtime construction creates and owns the store instance.



373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/little_ghost/configuration.rb', line 373

def session_store=(value)
  ensure_configuration_open!
  unless value.is_a?(Hash)
    raise ArgumentError, "session_store must be a hash with a provider"
  end

  provider = value[:provider]
  change_configuration do
    @configuration_values[:session_store] = value.merge(
      provider: component_class(provider, SessionStore, :session_store)
    )
  end
end

#settings(root: nil) ⇒ Object

:nodoc:



509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/little_ghost/configuration.rb', line 509

def settings(root: nil) # :nodoc:
  requested_root = root && canonical_root(root)
  values = configuration_values.dup
  values[:prompt_paths] = Array(values[:prompt_paths]).dup
  values[:skill_paths] = Array(values[:skill_paths]).dup
  values[:instrumentation_subscribers] = Array(values[:instrumentation_subscribers]).dup
  values[:runtime_hooks] = Array(values[:runtime_hooks]).dup
  values[:model_resolver] = model_resolver
  values[:default_model] = values[:model_resolver].default_model
  values[:root] = requested_root || values[:root] || self.root
  values
end

#skill_pathsObject

Skill lookup paths in precedence order. The Array is mutable until the shared Runtime is built.



494
# File 'lib/little_ghost/configuration.rb', line 494

def skill_paths = configuration_values[:skill_paths]

#skill_paths=(value) ⇒ Object

Replaces skill lookup paths with value converted to an Array.



497
498
499
# File 'lib/little_ghost/configuration.rb', line 497

def skill_paths=(value)
  change_configuration { configuration_values[:skill_paths] = Array(value) }
end

#skill_resource_rootObject

Optional trusted root exposed to skills for resource lookup.



502
# File 'lib/little_ghost/configuration.rb', line 502

def skill_resource_root = configuration_values[:skill_resource_root]

#skill_resource_root=(value) ⇒ Object

Replaces the trusted skill resource root for new runtimes.



505
506
507
# File 'lib/little_ghost/configuration.rb', line 505

def skill_resource_root=(value)
  change_configuration { configuration_values[:skill_resource_root] = value }
end

#workspaceObject

Workspace declaration used for subsequently built runtimes.



182
183
# File 'lib/little_ghost/configuration.rb', line 182

def workspace = configuration_values[:workspace]
# Sandbox declaration used for subsequently built runtimes.

#workspace=(value) ⇒ Object

Selects the Workspace subclass instantiated for each run.



356
357
358
359
360
# File 'lib/little_ghost/configuration.rb', line 356

def workspace=(value)
  change_configuration do
    @configuration_values[:workspace] = component_class(value, Workspace, :workspace)
  end
end