little_ghost 0.1.0 → 0.2.1

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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -47
  3. data/docs/guides/{Core Concepts.md → core_concepts.md} +40 -20
  4. data/docs/guides/getting_started.md +164 -0
  5. data/lib/little_ghost/ag_ui/adapter.rb +1 -1
  6. data/lib/little_ghost/agent/context_management.rb +1 -4
  7. data/lib/little_ghost/agent.rb +94 -26
  8. data/lib/little_ghost/agent_builder.rb +1 -1
  9. data/lib/little_ghost/configuration.rb +210 -29
  10. data/lib/little_ghost/data/model_catalog.json +19178 -0
  11. data/lib/little_ghost/errors.rb +6 -0
  12. data/lib/little_ghost/invocation.rb +2 -17
  13. data/lib/little_ghost/model.rb +53 -33
  14. data/lib/little_ghost/model_capabilities.rb +6 -5
  15. data/lib/little_ghost/model_resolver.rb +316 -0
  16. data/lib/little_ghost/models/catalog/models_dev_source.rb +62 -0
  17. data/lib/little_ghost/models/catalog/source.rb +30 -0
  18. data/lib/little_ghost/models/catalog.rb +154 -0
  19. data/lib/little_ghost/models/catalog_snapshot.rb +44 -0
  20. data/lib/little_ghost/models/configuration.rb +43 -0
  21. data/lib/little_ghost/models/details.rb +48 -0
  22. data/lib/little_ghost/models/target.rb +29 -0
  23. data/lib/little_ghost/provider_registry.rb +75 -0
  24. data/lib/little_ghost/providers/anthropic/catalog_source.rb +31 -0
  25. data/lib/little_ghost/providers/anthropic.rb +222 -0
  26. data/lib/little_ghost/providers/base.rb +46 -0
  27. data/lib/little_ghost/providers/bedrock/aws_protocol.rb +132 -0
  28. data/lib/little_ghost/providers/bedrock/catalog_source.rb +200 -0
  29. data/lib/little_ghost/providers/bedrock/credential_resolver.rb +123 -0
  30. data/lib/little_ghost/providers/bedrock/http_client.rb +78 -0
  31. data/lib/little_ghost/providers/bedrock.rb +23 -15
  32. data/lib/little_ghost/providers/configuration.rb +79 -0
  33. data/lib/little_ghost/providers/gemini/catalog_source.rb +35 -0
  34. data/lib/little_ghost/providers/gemini.rb +204 -0
  35. data/lib/little_ghost/providers/open_router/catalog_source.rb +42 -0
  36. data/lib/little_ghost/providers/open_router.rb +6 -2
  37. data/lib/little_ghost/providers/openai_compatible.rb +19 -23
  38. data/lib/little_ghost/providers/vertex_ai/credential_resolver.rb +90 -0
  39. data/lib/little_ghost/providers/vertex_ai.rb +38 -0
  40. data/lib/little_ghost/run.rb +7 -7
  41. data/lib/little_ghost/runtime.rb +10 -7
  42. data/lib/little_ghost/sandbox.rb +5 -5
  43. data/lib/little_ghost/session_store.rb +3 -3
  44. data/lib/little_ghost/structured_output.rb +2 -8
  45. data/lib/little_ghost/support/http_client.rb +186 -0
  46. data/lib/little_ghost/{providers → support}/sse_parser.rb +1 -1
  47. data/lib/little_ghost/tool.rb +5 -1
  48. data/lib/little_ghost/tool_registry.rb +2 -3
  49. data/lib/little_ghost/version.rb +1 -1
  50. data/lib/little_ghost/workflow.rb +1 -1
  51. data/lib/little_ghost.rb +21 -15
  52. metadata +28 -7
  53. data/docs/guides/Getting Started.md +0 -187
  54. data/lib/little_ghost/default_model_registry.rb +0 -71
  55. data/lib/little_ghost/model_registry.rb +0 -173
  56. data/lib/little_ghost/providers/http_transport.rb +0 -149
@@ -35,12 +35,12 @@ module LittleGhost
35
35
  # Capabilities such as skills, context management, loop detection, and
36
36
  # delegation remain inactive until their DSL methods are called.
37
37
  #
38
- # The class-level +ask+ helper creates a standalone entrypoint and returns a
39
- # completed Run. Create a standalone instance explicitly to reuse one Runtime
40
- # or call +stream_ask+ for StreamEvent objects. Runtimes build bound instances
41
- # internally; their +call+ method returns a RunResult and their +stream+ method
42
- # follows the owning run's single-execution lifecycle. Closing an agent closes
43
- # owned tools and any standalone workspace and sandbox.
38
+ # The class-level +ask+ and +stream_ask+ helpers create fresh standalone
39
+ # entrypoints. Create an instance explicitly to reuse one Runtime across
40
+ # calls. Runtimes build bound instances internally; their +call+ method
41
+ # returns a RunResult and their +stream+ method follows the owning run's
42
+ # single-execution lifecycle. Closing an agent closes owned tools and any
43
+ # standalone workspace and sandbox.
44
44
  # LittleGhost::Agent.ask uses <tt>You are a helpful agent.</tt> as its system
45
45
  # prompt. Subclasses continue to use their inline or conventional prompts.
46
46
  #
@@ -89,6 +89,19 @@ module LittleGhost
89
89
  new.ask(message, **options)
90
90
  end
91
91
 
92
+ # Streams +message+ through a fresh standalone entrypoint and returns an
93
+ # Enumerator of LittleGhost::StreamEvent objects. Invocation +options+
94
+ # are forwarded to #stream_ask.
95
+ #
96
+ # Create an instance explicitly when reusing one Runtime across calls.
97
+ def stream_ask(message, **options)
98
+ stream = nil
99
+ Enumerator.new do |events|
100
+ stream ||= new.stream_ask(message, **options)
101
+ stream.each { |event| events << event }
102
+ end
103
+ end
104
+
92
105
  # :call-seq:
93
106
  # agent_id() -> String
94
107
  # agent_id(value) -> String
@@ -121,23 +134,35 @@ module LittleGhost
121
134
  end
122
135
 
123
136
  # :call-seq:
124
- # model() -> String, Proc, nil
125
- # model(role) -> String
137
+ # model() -> String, Symbol, Hash, Proc, nil
138
+ # model(role_or_target) -> String, Symbol
139
+ # model(provider:, model:, **settings) -> Hash
126
140
  # model { |invocation| ... } -> Proc
127
141
  #
128
- # The logical model role for this agent.
142
+ # Selects this agent's model by logical role, canonical
143
+ # <tt>provider:model-id</tt> target, or an inline mapping with +provider+,
144
+ # +model+, and trusted model settings. The provider names a configured
145
+ # connection, not necessarily its adapter.
129
146
  #
130
- # Pass a block to choose a role from each Invocation at run time.
147
+ # Pass a block to choose any supported form from each Invocation at run
148
+ # time. Inline mappings use flat settings, for example:
149
+ #
150
+ # model(provider: "openai", model: "gpt-5.6-luna", reasoning_effort: "high")
131
151
  def model(*values, &block)
132
152
  return model_value if values.empty? && !block
153
+ if block && !values.empty?
154
+ raise ArgumentError, "model accepts either one selection or a block"
155
+ end
156
+ if values.length > 1
157
+ raise ArgumentError, "model accepts one selection"
158
+ end
133
159
 
134
- self.model_value = block || values.fetch(0).to_s
160
+ self.model_value = block || copy_model_selection(values.fetch(0))
135
161
  end
136
162
 
137
- def model_role(invocation) # :nodoc:
163
+ def model_selection(invocation) # :nodoc:
138
164
  value = model_value
139
- resolved = value.respond_to?(:call) ? value.call(invocation) : value
140
- resolved&.to_s
165
+ value.is_a?(Proc) ? value.call(invocation) : value
141
166
  end
142
167
 
143
168
  # :call-seq:
@@ -350,6 +375,32 @@ module LittleGhost
350
375
 
351
376
  private
352
377
 
378
+ def copy_model_selection(value)
379
+ case value
380
+ when String
381
+ value.dup.freeze
382
+ when Symbol
383
+ value
384
+ when Hash
385
+ value.to_h { |key, child| [key, copy_model_selection_value(child)] }.freeze
386
+ else
387
+ raise ConfigurationError, "model must be a role, provider:model target, or configuration mapping"
388
+ end
389
+ end
390
+
391
+ def copy_model_selection_value(value)
392
+ case value
393
+ when Hash
394
+ value.to_h { |key, child| [key, copy_model_selection_value(child)] }.freeze
395
+ when Array
396
+ value.map { |child| copy_model_selection_value(child) }.freeze
397
+ when String
398
+ value.dup.freeze
399
+ else
400
+ value
401
+ end
402
+ end
403
+
353
404
  def validate_result_schema_keywords!(schema, path = "$")
354
405
  unsupported = schema.keys - RESULT_SCHEMA_KEYWORDS
355
406
  unless unsupported.empty?
@@ -455,9 +506,9 @@ module LittleGhost
455
506
 
456
507
  # Creates either a standalone entrypoint or a run-scoped agent.
457
508
  #
458
- # Calling <tt>new</tt> without +model+ and +run+ creates the console-friendly
459
- # standalone form. Runtime builders supply the remaining dependencies and
460
- # apply class-level limits and declarations.
509
+ # Calling <tt>new</tt> without +model+ and +run+ creates the standalone form
510
+ # used by +ask+ and +stream_ask+. Runtime builders supply the remaining
511
+ # dependencies and apply class-level limits and declarations.
461
512
  def initialize(
462
513
  model: nil,
463
514
  runtime: nil,
@@ -563,7 +614,7 @@ module LittleGhost
563
614
  result
564
615
  end
565
616
 
566
- # Console-friendly name for +#call+.
617
+ # Runs +message+ to completion through the standalone or run-scoped agent.
567
618
  def ask(message, **options)
568
619
  call(message, **options)
569
620
  end
@@ -690,7 +741,13 @@ module LittleGhost
690
741
  if @standalone
691
742
  raise ArgumentError, "input is required" if input.nil?
692
743
 
693
- return build_run(entrypoint_payload(input, {})).each
744
+ return build_run(entrypoint_payload(input, {
745
+ history:,
746
+ context:,
747
+ settings:,
748
+ template_paths:,
749
+ deadline_at: deadline
750
+ }.compact)).each
694
751
  end
695
752
 
696
753
  raise ArgumentError, "input is required" if input.nil?
@@ -744,8 +801,19 @@ module LittleGhost
744
801
  end
745
802
  end
746
803
 
747
- # Console-friendly name for +#stream+.
804
+ # Streams +message+ through the standalone or run-scoped agent.
805
+ # Standalone +options+ become Invocation fields; run-scoped options are
806
+ # forwarded to #stream.
748
807
  def stream_ask(message, **options)
808
+ if @standalone
809
+ options[:deadline_at] = options.delete(:deadline) if options.key?(:deadline)
810
+ stream = nil
811
+ return Enumerator.new do |events|
812
+ stream ||= build_run(entrypoint_payload(message, options)).each
813
+ stream.each { |event| events << event }
814
+ end
815
+ end
816
+
749
817
  stream(message, **options)
750
818
  end
751
819
 
@@ -1146,7 +1214,7 @@ module LittleGhost
1146
1214
  outcome: :completed,
1147
1215
  turn: turn + 1
1148
1216
  )
1149
- metadata = model.respond_to?(:metadata) ? model.metadata : {}
1217
+ metadata = model.details.to_h.merge(model_role: model.role)
1150
1218
  finish_instrumentation(
1151
1219
  agent_handle,
1152
1220
  outcome: :completed,
@@ -1228,7 +1296,7 @@ module LittleGhost
1228
1296
  diagnostic: {exception: diagnostic_exception(error)},
1229
1297
  **usage_attributes(context.usage)
1230
1298
  )
1231
- metadata = model.respond_to?(:metadata) ? model.metadata : {}
1299
+ metadata = model.details.to_h.merge(model_role: model.role)
1232
1300
  emit(events, :invocation_error, error:, usage: context.usage, metadata:)
1233
1301
  raise
1234
1302
  end
@@ -1874,7 +1942,7 @@ module LittleGhost
1874
1942
  outcome: :completed,
1875
1943
  turn:
1876
1944
  )
1877
- metadata = model.respond_to?(:metadata) ? model.metadata : {}
1945
+ metadata = model.details.to_h.merge(model_role: model.role)
1878
1946
  finish_instrumentation(
1879
1947
  agent_handle,
1880
1948
  outcome: :completed,
@@ -1994,9 +2062,9 @@ module LittleGhost
1994
2062
 
1995
2063
  def model_attributes
1996
2064
  {
1997
- model_id: model.respond_to?(:id) ? model.id : nil,
1998
- model_role: model.respond_to?(:role) ? model.role : nil,
1999
- model_provider: model.respond_to?(:provider_name) ? model.provider_name : model.class.name
2065
+ model_id: model.model_id,
2066
+ model_role: model.role,
2067
+ model_provider: model.target.provider
2000
2068
  }.compact
2001
2069
  end
2002
2070
 
@@ -171,7 +171,7 @@ module LittleGhost
171
171
 
172
172
  def close_tools(tools)
173
173
  Array(tools).reverse_each do |tool|
174
- tool.close if tool.is_a?(Tool) && tool.respond_to?(:close)
174
+ tool.close if tool.is_a?(Tool)
175
175
  rescue
176
176
  nil
177
177
  end
@@ -8,7 +8,6 @@ module LittleGhost
8
8
  # instrumentation, and runtime hooks for an application.
9
9
  #
10
10
  # LittleGhost.configure do |config|
11
- # config.models CustomerSupportModels
12
11
  # config.default_model :customer_support
13
12
  # config.service_name "support-api"
14
13
  # end
@@ -30,7 +29,7 @@ module LittleGhost
30
29
  # not from an unverified request field.
31
30
  class Configuration
32
31
  FILE_LOAD_MUTEX = Mutex.new # :nodoc:
33
- CONFIGURATION_KEYS = %i[invocation models default_model service_name].freeze # :nodoc:
32
+ CONFIGURATION_KEYS = %i[invocation service_name].freeze # :nodoc:
34
33
  DEFAULT_PROMPT_PATHS = ["app/prompts"].freeze # :nodoc:
35
34
  DEFAULT_SKILL_PATHS = ["app/skills"].freeze # :nodoc:
36
35
 
@@ -43,24 +42,6 @@ module LittleGhost
43
42
  # invocation(value) -> value
44
43
 
45
44
  ##
46
- # The model registry class or instance used to resolve logical roles. By
47
- # default, +DefaultModelRegistry+ selects a built-in provider from supported
48
- # environment variables.
49
- #
50
- # :method: models
51
- # :call-seq:
52
- # models() -> value
53
- # models(value) -> value
54
-
55
- ##
56
- # The fallback logical model role for agents without an explicit role.
57
- # Values are normalized to strings.
58
- #
59
- # :method: default_model
60
- # :call-seq:
61
- # default_model() -> String, nil
62
- # default_model(value) -> String
63
-
64
45
  ##
65
46
  # The low-cardinality service name attached to instrumentation.
66
47
  #
@@ -72,7 +53,7 @@ module LittleGhost
72
53
  define_method(name) do |value = :__read__|
73
54
  return configuration_values[name] if value == :__read__
74
55
 
75
- configuration_values[name] = (name == :default_model) ? value.to_s : value
56
+ configuration_values[name] = value
76
57
  end
77
58
  end
78
59
 
@@ -83,17 +64,12 @@ module LittleGhost
83
64
  # invocation=(value) -> value
84
65
 
85
66
  ##
86
- # Replaces the model registry declaration for subsequently built runtimes.
87
- # :method: models=
67
+ # Replaces the model resolver declaration for subsequently built runtimes.
68
+ # :method: model_resolver=
88
69
  # :call-seq:
89
- # models=(value) -> value
70
+ # model_resolver=(value) -> value
90
71
 
91
72
  ##
92
- # Replaces the fallback logical model role and normalizes it to a String.
93
- # :method: default_model=
94
- # :call-seq:
95
- # default_model=(value) -> String
96
-
97
73
  ##
98
74
  # Replaces the service name attached to telemetry from new runtimes.
99
75
  # :method: service_name=
@@ -124,6 +100,9 @@ module LittleGhost
124
100
  @configuration_values[:instrumentation_subscribers]
125
101
  ).dup
126
102
  @configuration_values[:runtime_hooks] = Array(@configuration_values[:runtime_hooks]).dup
103
+ @configuration_values[:provider_adapters] = @configuration_values.fetch(:provider_adapters, {}).dup
104
+ @configuration_values[:catalog_sources] = Array(@configuration_values[:catalog_sources]).dup
105
+ @configuration_values[:provider_credentials] ||= nil
127
106
  end
128
107
 
129
108
  # Yields this builder for setup and returns the same instance.
@@ -139,6 +118,153 @@ module LittleGhost
139
118
  # Session-store declaration used for subsequently built runtimes.
140
119
  def session_store = configuration_values[:session_store]
141
120
 
121
+ # Trusted provider connections for the default or custom resolver.
122
+ def providers(value = :__read__)
123
+ return configuration_values[:providers] if value == :__read__
124
+
125
+ unless value.is_a?(Hash) || value.is_a?(Providers::Configuration)
126
+ raise ArgumentError, "providers must be a Hash or LittleGhost::Providers::Configuration"
127
+ end
128
+
129
+ configuration_values[:providers] = value
130
+ reset_model_resolver
131
+ value
132
+ end
133
+
134
+ # Replaces trusted provider connections for subsequently built runtimes.
135
+ def providers=(value)
136
+ providers(value)
137
+ end
138
+
139
+ # Logical model profiles for the default resolver. Role names cannot contain
140
+ # a colon because that syntax identifies a canonical model target.
141
+ def models(value = :__read__)
142
+ return configuration_values[:models] if value == :__read__
143
+ raise ArgumentError, "models must be a Hash" unless value.is_a?(Hash)
144
+
145
+ configuration_values[:models] = value
146
+ reset_model_resolver
147
+ value
148
+ end
149
+
150
+ # Replaces logical model profiles for subsequently built runtimes.
151
+ def models=(value)
152
+ models(value)
153
+ end
154
+
155
+ # Fallback logical role for the default resolver.
156
+ def default_model(value = :__read__)
157
+ return configuration_values[:default_model] if value == :__read__
158
+
159
+ configuration_values[:default_model] = value.to_s
160
+ reset_model_resolver
161
+ value.to_s
162
+ end
163
+
164
+ # Replaces the fallback logical role and normalizes it to a String.
165
+ def default_model=(value)
166
+ default_model(value)
167
+ end
168
+
169
+ # Provider YAML path. The conventional path is optional; an explicitly set
170
+ # path must exist when a runtime is built.
171
+ def providers_path(value = :__read__) = configuration_path(:providers_path, "providers.yml", value)
172
+
173
+ # Replaces the provider YAML path for subsequently built runtimes.
174
+ def providers_path=(value)
175
+ providers_path(value)
176
+ end
177
+
178
+ # Model YAML path. The conventional path is optional; an explicitly set
179
+ # path must exist when a runtime is built.
180
+ def models_path(value = :__read__) = configuration_path(:models_path, "models.yml", value)
181
+
182
+ # Replaces the model YAML path for subsequently built runtimes.
183
+ def models_path=(value)
184
+ models_path(value)
185
+ end
186
+
187
+ # Installs a complete resolver override for subsequently built runtimes.
188
+ def model_resolver(value = :__read__)
189
+ if value != :__read__
190
+ validate_model_resolver_class!(value)
191
+
192
+ configuration_values[:model_resolver] = value
193
+ @resolved_model_resolver = nil
194
+ return value
195
+ end
196
+
197
+ @model_resolver_mutex ||= Mutex.new
198
+ @model_resolver_mutex.synchronize do
199
+ @resolved_model_resolver ||= begin
200
+ providers = resolved_providers
201
+ credential_resolver = configuration_values[:provider_credentials] || providers&.method(:credentials)
202
+ configured = configuration_values[:model_resolver]
203
+ if configured
204
+ validate_model_resolver_configuration!
205
+ configured.new(
206
+ providers:,
207
+ provider_adapters: configuration_values[:provider_adapters],
208
+ catalog_sources: configuration_values[:catalog_sources],
209
+ credential_resolver:
210
+ )
211
+ else
212
+ profiles, file_default = resolved_models
213
+ ModelResolver.new(
214
+ providers:,
215
+ profiles:,
216
+ default_model: configuration_values.fetch(:default_model, file_default),
217
+ provider_adapters: configuration_values[:provider_adapters],
218
+ catalog_sources: configuration_values[:catalog_sources],
219
+ credential_resolver:
220
+ )
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ def model_resolver=(value)
227
+ model_resolver(value)
228
+ end
229
+
230
+ # Registers a provider adapter factory under +name+.
231
+ def provider_adapter(name, callable = nil, &factory)
232
+ implementation = factory || callable
233
+ if implementation.is_a?(Class)
234
+ unless implementation <= Providers::Base
235
+ raise ArgumentError, "provider adapter class must inherit LittleGhost::Providers::Base"
236
+ end
237
+ elsif !implementation.respond_to?(:call)
238
+ raise ArgumentError, "provider adapter must be a Providers::Base class or callable factory"
239
+ end
240
+
241
+ configuration_values[:provider_adapters][name.to_s] = implementation
242
+ @resolved_model_resolver = nil
243
+ implementation
244
+ end
245
+
246
+ # Adds an explicit catalog source. Sources refresh only when callers invoke
247
+ # ModelResolver#refresh!.
248
+ def catalog_source(source)
249
+ raise ArgumentError, "catalog source must be a Models::Catalog::Source" unless source.is_a?(Models::Catalog::Source)
250
+
251
+ configuration_values[:catalog_sources] << source
252
+ @resolved_model_resolver = nil
253
+ source
254
+ end
255
+
256
+ # Installs a trusted callable that returns credential options for a named
257
+ # provider connection when each executable model is constructed.
258
+ def provider_credentials(callable = nil, &resolver)
259
+ value = resolver || callable
260
+ return configuration_values[:provider_credentials] unless value
261
+ raise ArgumentError, "provider credential resolver must be callable" unless value.respond_to?(:call)
262
+
263
+ configuration_values[:provider_credentials] = value
264
+ @resolved_model_resolver = nil
265
+ value
266
+ end
267
+
142
268
  # Selects the Workspace subclass instantiated for each run.
143
269
  def workspace=(value)
144
270
  @configuration_values[:workspace] = component_class(value, Workspace, :workspace)
@@ -286,6 +412,8 @@ module LittleGhost
286
412
  values[:skill_paths] = Array(values[:skill_paths]).dup
287
413
  values[:instrumentation_subscribers] = Array(values[:instrumentation_subscribers]).dup
288
414
  values[:runtime_hooks] = Array(values[:runtime_hooks]).dup
415
+ values[:model_resolver] = model_resolver
416
+ values[:default_model] = values[:model_resolver].default_model
289
417
  values[:root] = requested_root || values[:root] || self.root
290
418
  values
291
419
  end
@@ -313,6 +441,59 @@ module LittleGhost
313
441
 
314
442
  attr_reader :configuration_values
315
443
 
444
+ def validate_model_resolver_class!(value)
445
+ return if value.is_a?(Class) && value <= ModelResolver
446
+
447
+ raise ArgumentError, "model_resolver must be a LittleGhost::ModelResolver subclass"
448
+ end
449
+
450
+ def configuration_path(key, filename, value)
451
+ if value != :__read__
452
+ configuration_values[key] = value
453
+ reset_model_resolver
454
+ return value
455
+ end
456
+
457
+ configuration_values.fetch(key) { root.join("config/little_ghost", filename) }
458
+ end
459
+
460
+ def resolved_providers
461
+ return unless configuration_values.key?(:providers) || (path = resolved_configuration_file(:providers_path, "providers.yml"))
462
+
463
+ configured = configuration_values[:providers]
464
+ return configured if configured.is_a?(Providers::Configuration)
465
+ return Providers::Configuration.new(configured) if configuration_values.key?(:providers)
466
+
467
+ Models::Configuration.providers(path)
468
+ end
469
+
470
+ def resolved_models
471
+ return [configuration_values[:models], nil] if configuration_values.key?(:models)
472
+
473
+ path = resolved_configuration_file(:models_path, "models.yml")
474
+ path ? Models::Configuration.models(path) : [nil, nil]
475
+ end
476
+
477
+ def resolved_configuration_file(key, filename)
478
+ explicit = configuration_values.key?(key)
479
+ path = Pathname(configuration_values.fetch(key) { root.join("config/little_ghost", filename) })
480
+ path = root.join(path) unless path.absolute?
481
+ return path if path.file?
482
+ raise ConfigurationError, "LittleGhost configuration file does not exist: #{path}" if explicit
483
+ end
484
+
485
+ def validate_model_resolver_configuration!
486
+ conflicting = %i[models models_path default_model].select { |key| configuration_values.key?(key) }
487
+ return if conflicting.empty?
488
+
489
+ raise ConfigurationError,
490
+ "custom model_resolver cannot be combined with #{conflicting.join(", ")}"
491
+ end
492
+
493
+ def reset_model_resolver
494
+ @resolved_model_resolver = nil
495
+ end
496
+
316
497
  def component_class(value, base_class, name)
317
498
  unless value.is_a?(Class) && value <= base_class
318
499
  raise ArgumentError, "#{name} must be a #{base_class} class"