little_ghost 0.2.1 → 0.4.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +72 -74
  3. data/docs/guides/assemblies.md +286 -0
  4. data/docs/guides/core_concepts.md +159 -135
  5. data/docs/guides/getting_started.md +114 -83
  6. data/docs/guides/production.md +187 -0
  7. data/docs/guides/prompt_views.md +132 -0
  8. data/lib/little_ghost/ag_ui/adapter.rb +3 -3
  9. data/lib/little_ghost/agent/delegation.rb +35 -8
  10. data/lib/little_ghost/agent/tool_loop.rb +2 -1
  11. data/lib/little_ghost/agent.rb +280 -326
  12. data/lib/little_ghost/agent_builder.rb +20 -4
  13. data/lib/little_ghost/agent_factory.rb +3 -0
  14. data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
  15. data/lib/little_ghost/assembly.rb +345 -0
  16. data/lib/little_ghost/assembly_builder.rb +497 -0
  17. data/lib/little_ghost/assembly_execution.rb +535 -0
  18. data/lib/little_ghost/configuration.rb +263 -39
  19. data/lib/little_ghost/content.rb +5 -5
  20. data/lib/little_ghost/data_map.rb +209 -0
  21. data/lib/little_ghost/errors.rb +10 -2
  22. data/lib/little_ghost/execution.rb +206 -0
  23. data/lib/little_ghost/graph.rb +930 -0
  24. data/lib/little_ghost/message.rb +4 -4
  25. data/lib/little_ghost/model_resolver.rb +2 -2
  26. data/lib/little_ghost/prompt_resolver.rb +2 -0
  27. data/lib/little_ghost/run.rb +190 -64
  28. data/lib/little_ghost/run_context.rb +33 -20
  29. data/lib/little_ghost/run_result.rb +22 -11
  30. data/lib/little_ghost/runtime/hook.rb +9 -4
  31. data/lib/little_ghost/runtime.rb +134 -36
  32. data/lib/little_ghost/sandbox.rb +1 -1
  33. data/lib/little_ghost/session.rb +12 -23
  34. data/lib/little_ghost/session_store.rb +9 -5
  35. data/lib/little_ghost/session_stores/agent_core_memory.rb +64 -56
  36. data/lib/little_ghost/session_stores/filesystem.rb +261 -0
  37. data/lib/little_ghost/session_stores/memory.rb +7 -0
  38. data/lib/little_ghost/subagents/manager.rb +42 -42
  39. data/lib/little_ghost/support/executor.rb +14 -2
  40. data/lib/little_ghost/support/loader.rb +2 -2
  41. data/lib/little_ghost/support.rb +15 -3
  42. data/lib/little_ghost/swarm.rb +439 -0
  43. data/lib/little_ghost/tool.rb +88 -20
  44. data/lib/little_ghost/tools/write_todos.rb +6 -1
  45. data/lib/little_ghost/tracing/open_telemetry.rb +14 -3
  46. data/lib/little_ghost/unrestricted_sandbox.rb +1 -1
  47. data/lib/little_ghost/version.rb +1 -1
  48. data/lib/little_ghost/workflow.rb +224 -90
  49. data/lib/little_ghost.rb +36 -25
  50. metadata +17 -5
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pathname"
4
+ require "monitor"
4
5
 
5
6
  module LittleGhost
6
7
  # Configure shared services and lookup rules before agents start.
@@ -19,10 +20,12 @@ module LittleGhost
19
20
  # under the application root. Applications may append shared roots or replace
20
21
  # the arrays entirely.
21
22
  #
22
- # Configuration is a mutable builder, while each Runtime owns a settings
23
- # snapshot. The first runtime for a root loads +config/little_ghost.rb+ once;
24
- # later mutations do not alter that runtime, and one configuration cannot load
25
- # files for two different roots.
23
+ # Configuration is a mutable application builder until its shared Runtime is
24
+ # first used. A successful #runtime call locks the builder so standalone
25
+ # Agents and Assemblies keep one stable setup. Configure the application
26
+ # before its first entrypoint call. Explicit Runtime construction remains an
27
+ # advanced way to take an independent snapshot without selecting the shared
28
+ # default.
26
29
  #
27
30
  # Session actor resolvers belong at an authentication boundary. Multi-tenant
28
31
  # applications should derive actor identity from trusted authenticated state,
@@ -32,7 +35,6 @@ module LittleGhost
32
35
  CONFIGURATION_KEYS = %i[invocation service_name].freeze # :nodoc:
33
36
  DEFAULT_PROMPT_PATHS = ["app/prompts"].freeze # :nodoc:
34
37
  DEFAULT_SKILL_PATHS = ["app/skills"].freeze # :nodoc:
35
-
36
38
  ##
37
39
  # The request envelope class used to parse application payloads.
38
40
  #
@@ -53,7 +55,7 @@ module LittleGhost
53
55
  define_method(name) do |value = :__read__|
54
56
  return configuration_values[name] if value == :__read__
55
57
 
56
- configuration_values[name] = value
58
+ change_configuration { configuration_values[name] = value }
57
59
  end
58
60
  end
59
61
 
@@ -85,6 +87,8 @@ module LittleGhost
85
87
  # Collection settings are copied so callers can safely reuse their input
86
88
  # arrays after construction.
87
89
  def initialize(values = {})
90
+ @lifecycle_monitor = Monitor.new
91
+ @runtime_condition = @lifecycle_monitor.new_cond
88
92
  @configuration_values = {
89
93
  prompt_paths: DEFAULT_PROMPT_PATHS.dup,
90
94
  skill_paths: DEFAULT_SKILL_PATHS.dup,
@@ -107,10 +111,73 @@ module LittleGhost
107
111
 
108
112
  # Yields this builder for setup and returns the same instance.
109
113
  def configure
110
- yield self if block_given?
114
+ change_configuration { yield self } if block_given?
111
115
  self
112
116
  end
113
117
 
118
+ # Returns the shared Runtime for this configuration, building it on first
119
+ # use. Once construction succeeds, the configuration is locked so every
120
+ # standalone entrypoint continues to use one stable application setup. The
121
+ # conventional configuration file may finish loading during construction;
122
+ # other writes are rejected. A failed build leaves the configuration
123
+ # editable for a later attempt.
124
+ def runtime
125
+ build_generation = @lifecycle_monitor.synchronize do
126
+ loop do
127
+ return @default_runtime if @default_runtime
128
+ if @runtime_building
129
+ if @runtime_builder_thread.equal?(Thread.current)
130
+ raise ConfigurationError, "LittleGhost.runtime cannot be called while the shared Runtime is starting"
131
+ end
132
+
133
+ waiting_generation = @runtime_generation
134
+ @runtime_condition.wait
135
+ return @default_runtime if @default_runtime
136
+ if @runtime_failure_generation == waiting_generation
137
+ raise @runtime_failure
138
+ end
139
+ else
140
+ sealed_values = freeze_configuration_copy(@configuration_values)
141
+ @runtime_generation = @runtime_generation.to_i + 1
142
+ @runtime_building = true
143
+ @runtime_builder_thread = Thread.current
144
+ @configuration_values = sealed_values
145
+ break @runtime_generation
146
+ end
147
+ end
148
+ end
149
+
150
+ begin
151
+ runtime_root = root
152
+ load_file!(root: runtime_root)
153
+ runtime_settings = settings(root: runtime_root)
154
+ built = Runtime.new(configuration: self, settings: runtime_settings)
155
+ @lifecycle_monitor.synchronize do
156
+ configuration_values.freeze
157
+ @default_runtime = built
158
+ end
159
+ built
160
+ rescue => error
161
+ editable_values = if @configuration_values.frozen?
162
+ copy_configuration_value(@configuration_values)
163
+ else
164
+ @configuration_values
165
+ end
166
+ @lifecycle_monitor.synchronize do
167
+ @configuration_values = editable_values
168
+ @runtime_failure = error
169
+ @runtime_failure_generation = build_generation
170
+ end
171
+ raise
172
+ ensure
173
+ @lifecycle_monitor.synchronize do
174
+ @runtime_building = false
175
+ @runtime_builder_thread = nil
176
+ @runtime_condition.broadcast
177
+ end
178
+ end
179
+ end
180
+
114
181
  # Workspace declaration used for subsequently built runtimes.
115
182
  def workspace = configuration_values[:workspace]
116
183
  # Sandbox declaration used for subsequently built runtimes.
@@ -122,12 +189,15 @@ module LittleGhost
122
189
  def providers(value = :__read__)
123
190
  return configuration_values[:providers] if value == :__read__
124
191
 
192
+ ensure_configuration_open!
125
193
  unless value.is_a?(Hash) || value.is_a?(Providers::Configuration)
126
194
  raise ArgumentError, "providers must be a Hash or LittleGhost::Providers::Configuration"
127
195
  end
128
196
 
129
- configuration_values[:providers] = value
130
- reset_model_resolver
197
+ change_configuration do
198
+ configuration_values[:providers] = value
199
+ reset_model_resolver
200
+ end
131
201
  value
132
202
  end
133
203
 
@@ -140,10 +210,13 @@ module LittleGhost
140
210
  # a colon because that syntax identifies a canonical model target.
141
211
  def models(value = :__read__)
142
212
  return configuration_values[:models] if value == :__read__
213
+ ensure_configuration_open!
143
214
  raise ArgumentError, "models must be a Hash" unless value.is_a?(Hash)
144
215
 
145
- configuration_values[:models] = value
146
- reset_model_resolver
216
+ change_configuration do
217
+ configuration_values[:models] = value
218
+ reset_model_resolver
219
+ end
147
220
  value
148
221
  end
149
222
 
@@ -156,8 +229,10 @@ module LittleGhost
156
229
  def default_model(value = :__read__)
157
230
  return configuration_values[:default_model] if value == :__read__
158
231
 
159
- configuration_values[:default_model] = value.to_s
160
- reset_model_resolver
232
+ change_configuration do
233
+ configuration_values[:default_model] = value.to_s
234
+ reset_model_resolver
235
+ end
161
236
  value.to_s
162
237
  end
163
238
 
@@ -187,10 +262,13 @@ module LittleGhost
187
262
  # Installs a complete resolver override for subsequently built runtimes.
188
263
  def model_resolver(value = :__read__)
189
264
  if value != :__read__
265
+ ensure_configuration_open!
190
266
  validate_model_resolver_class!(value)
191
267
 
192
- configuration_values[:model_resolver] = value
193
- @resolved_model_resolver = nil
268
+ change_configuration do
269
+ configuration_values[:model_resolver] = value
270
+ @resolved_model_resolver = nil
271
+ end
194
272
  return value
195
273
  end
196
274
 
@@ -229,6 +307,7 @@ module LittleGhost
229
307
 
230
308
  # Registers a provider adapter factory under +name+.
231
309
  def provider_adapter(name, callable = nil, &factory)
310
+ ensure_configuration_open!
232
311
  implementation = factory || callable
233
312
  if implementation.is_a?(Class)
234
313
  unless implementation <= Providers::Base
@@ -238,18 +317,23 @@ module LittleGhost
238
317
  raise ArgumentError, "provider adapter must be a Providers::Base class or callable factory"
239
318
  end
240
319
 
241
- configuration_values[:provider_adapters][name.to_s] = implementation
242
- @resolved_model_resolver = nil
320
+ change_configuration do
321
+ configuration_values[:provider_adapters][name.to_s] = implementation
322
+ @resolved_model_resolver = nil
323
+ end
243
324
  implementation
244
325
  end
245
326
 
246
327
  # Adds an explicit catalog source. Sources refresh only when callers invoke
247
328
  # ModelResolver#refresh!.
248
329
  def catalog_source(source)
330
+ ensure_configuration_open!
249
331
  raise ArgumentError, "catalog source must be a Models::Catalog::Source" unless source.is_a?(Models::Catalog::Source)
250
332
 
251
- configuration_values[:catalog_sources] << source
252
- @resolved_model_resolver = nil
333
+ change_configuration do
334
+ configuration_values[:catalog_sources] << source
335
+ @resolved_model_resolver = nil
336
+ end
253
337
  source
254
338
  end
255
339
 
@@ -258,21 +342,28 @@ module LittleGhost
258
342
  def provider_credentials(callable = nil, &resolver)
259
343
  value = resolver || callable
260
344
  return configuration_values[:provider_credentials] unless value
345
+ ensure_configuration_open!
261
346
  raise ArgumentError, "provider credential resolver must be callable" unless value.respond_to?(:call)
262
347
 
263
- configuration_values[:provider_credentials] = value
264
- @resolved_model_resolver = nil
348
+ change_configuration do
349
+ configuration_values[:provider_credentials] = value
350
+ @resolved_model_resolver = nil
351
+ end
265
352
  value
266
353
  end
267
354
 
268
355
  # Selects the Workspace subclass instantiated for each run.
269
356
  def workspace=(value)
270
- @configuration_values[:workspace] = component_class(value, Workspace, :workspace)
357
+ change_configuration do
358
+ @configuration_values[:workspace] = component_class(value, Workspace, :workspace)
359
+ end
271
360
  end
272
361
 
273
362
  # Selects the Sandbox subclass instantiated around each run's workspace.
274
363
  def sandbox=(value)
275
- @configuration_values[:sandbox] = component_class(value, Sandbox, :sandbox)
364
+ change_configuration do
365
+ @configuration_values[:sandbox] = component_class(value, Sandbox, :sandbox)
366
+ end
276
367
  end
277
368
 
278
369
  # Selects session persistence with a +:provider+ and its constructor options.
@@ -280,12 +371,17 @@ module LittleGhost
280
371
  # The provider must be a SessionStore subclass. Runtime construction creates
281
372
  # and owns the store instance.
282
373
  def session_store=(value)
374
+ ensure_configuration_open!
283
375
  unless value.is_a?(Hash)
284
376
  raise ArgumentError, "session_store must be a hash with a provider"
285
377
  end
286
378
 
287
379
  provider = value[:provider]
288
- @configuration_values[:session_store] = value.merge(provider: component_class(provider, SessionStore, :session_store))
380
+ change_configuration do
381
+ @configuration_values[:session_store] = value.merge(
382
+ provider: component_class(provider, SessionStore, :session_store)
383
+ )
384
+ end
289
385
  end
290
386
 
291
387
  # Looks up an arbitrary setting by symbol or string-compatible name.
@@ -295,7 +391,7 @@ module LittleGhost
295
391
 
296
392
  # Adds or replaces an arbitrary setting.
297
393
  def []=(name, value)
298
- configuration_values[name.to_sym] = value
394
+ change_configuration { configuration_values[name.to_sym] = value }
299
395
  end
300
396
 
301
397
  # Replaces the application root after resolving it to a stable real path.
@@ -305,11 +401,12 @@ module LittleGhost
305
401
 
306
402
  # Adds an Instrumentation::Subscriber to each new runtime and returns it.
307
403
  def instrument(subscriber)
404
+ ensure_configuration_open!
308
405
  unless subscriber.is_a?(Instrumentation::Subscriber)
309
406
  raise ArgumentError, "instrumentation subscriber must be a LittleGhost::Instrumentation::Subscriber"
310
407
  end
311
408
 
312
- configuration_values[:instrumentation_subscribers] << subscriber
409
+ change_configuration { configuration_values[:instrumentation_subscribers] << subscriber }
313
410
  subscriber
314
411
  end
315
412
 
@@ -326,7 +423,7 @@ module LittleGhost
326
423
  def log_events_to(destination = :__read__)
327
424
  return Events.console_output if destination == :__read__
328
425
 
329
- Events.console_output = destination
426
+ change_configuration { Events.console_output = destination }
330
427
  end
331
428
 
332
429
  # Replaces the console destination for structured framework events.
@@ -336,11 +433,12 @@ module LittleGhost
336
433
 
337
434
  # Adds a Runtime::Hook subclass to each new runtime and returns it.
338
435
  def runtime_hook(hook_class)
436
+ ensure_configuration_open!
339
437
  unless hook_class.is_a?(Class) && hook_class <= Runtime::Hook
340
438
  raise ArgumentError, "runtime_hook must be a LittleGhost::Runtime::Hook class"
341
439
  end
342
440
 
343
- configuration_values[:runtime_hooks] << hook_class
441
+ change_configuration { configuration_values[:runtime_hooks] << hook_class }
344
442
  hook_class
345
443
  end
346
444
 
@@ -356,12 +454,13 @@ module LittleGhost
356
454
  def session_actor(value = :__read__, &resolver)
357
455
  return configuration_values[:session_actor] if value == :__read__ && !resolver
358
456
 
457
+ ensure_configuration_open!
359
458
  raise ArgumentError, "Provide a session actor resolver or a block, not both" if value != :__read__ && resolver
360
459
 
361
460
  configured = resolver || value
362
461
  raise ArgumentError, "session_actor must be callable" unless configured.respond_to?(:call)
363
462
 
364
- configuration_values[:session_actor] = configured
463
+ change_configuration { configuration_values[:session_actor] = configured }
365
464
  end
366
465
 
367
466
  # :call-seq:
@@ -374,27 +473,29 @@ module LittleGhost
374
473
  # resolved so runtimes and lookup paths share one stable boundary.
375
474
  def root(value = :__read__)
376
475
  if value != :__read__
377
- return configuration_values[:root] = canonical_root(value)
476
+ return change_configuration { configuration_values[:root] = canonical_root(value) }
378
477
  end
379
478
 
380
479
  configured = configuration_values[:root]
381
480
  configured ? canonical_root(configured) : inferred_root
382
481
  end
383
482
 
384
- # Mutable prompt lookup paths, in precedence order.
483
+ # Prompt lookup paths in precedence order. The Array is mutable until the
484
+ # shared Runtime is built.
385
485
  def prompt_paths = configuration_values[:prompt_paths]
386
486
 
387
487
  # Replaces prompt lookup paths with +value+ converted to an Array.
388
488
  def prompt_paths=(value)
389
- configuration_values[:prompt_paths] = Array(value)
489
+ change_configuration { configuration_values[:prompt_paths] = Array(value) }
390
490
  end
391
491
 
392
- # Mutable skill lookup paths, in precedence order.
492
+ # Skill lookup paths in precedence order. The Array is mutable until the
493
+ # shared Runtime is built.
393
494
  def skill_paths = configuration_values[:skill_paths]
394
495
 
395
496
  # Replaces skill lookup paths with +value+ converted to an Array.
396
497
  def skill_paths=(value)
397
- configuration_values[:skill_paths] = Array(value)
498
+ change_configuration { configuration_values[:skill_paths] = Array(value) }
398
499
  end
399
500
 
400
501
  # Optional trusted root exposed to skills for resource lookup.
@@ -402,7 +503,7 @@ module LittleGhost
402
503
 
403
504
  # Replaces the trusted skill resource root for new runtimes.
404
505
  def skill_resource_root=(value)
405
- configuration_values[:skill_resource_root] = value
506
+ change_configuration { configuration_values[:skill_resource_root] = value }
406
507
  end
407
508
 
408
509
  def settings(root: nil) # :nodoc:
@@ -429,7 +530,7 @@ module LittleGhost
429
530
  end
430
531
 
431
532
  path = File.join(requested_root, "config/little_ghost.rb")
432
- LittleGhost.with_configuration(self) { Kernel.load(path) } if File.file?(path)
533
+ load_configuration_file(path) if File.file?(path)
433
534
  @configuration_file_root = requested_root
434
535
  end
435
536
  end
@@ -439,7 +540,15 @@ module LittleGhost
439
540
 
440
541
  private
441
542
 
442
- attr_reader :configuration_values
543
+ def configuration_values
544
+ @lifecycle_monitor.synchronize do
545
+ if @runtime_building && !@runtime_builder_thread.equal?(Thread.current)
546
+ raise_locked_configuration!
547
+ end
548
+
549
+ @configuration_values
550
+ end
551
+ end
443
552
 
444
553
  def validate_model_resolver_class!(value)
445
554
  return if value.is_a?(Class) && value <= ModelResolver
@@ -449,8 +558,10 @@ module LittleGhost
449
558
 
450
559
  def configuration_path(key, filename, value)
451
560
  if value != :__read__
452
- configuration_values[key] = value
453
- reset_model_resolver
561
+ change_configuration do
562
+ configuration_values[key] = value
563
+ reset_model_resolver
564
+ end
454
565
  return value
455
566
  end
456
567
 
@@ -494,6 +605,119 @@ module LittleGhost
494
605
  @resolved_model_resolver = nil
495
606
  end
496
607
 
608
+ def change_configuration
609
+ @lifecycle_monitor.synchronize do
610
+ raise_locked_configuration! unless configuration_changes_allowed?
611
+
612
+ yield
613
+ end
614
+ end
615
+
616
+ def ensure_configuration_open!
617
+ @lifecycle_monitor.synchronize do
618
+ raise_locked_configuration! unless configuration_changes_allowed?
619
+ end
620
+ end
621
+
622
+ def raise_locked_configuration!
623
+ raise ConfigurationError,
624
+ "LittleGhost configuration is locked while its shared Runtime is starting or after it is ready; configure the application before its first Agent or Assembly call"
625
+ end
626
+
627
+ def configuration_changes_allowed?
628
+ return false if @default_runtime
629
+ return true unless @runtime_building
630
+
631
+ @configuration_file_loading && @runtime_builder_thread.equal?(Thread.current)
632
+ end
633
+
634
+ def load_configuration_file(path)
635
+ editable_values = copy_configuration_value(@configuration_values)
636
+ @lifecycle_monitor.synchronize do
637
+ @configuration_values = editable_values
638
+ @configuration_file_loading = true
639
+ end
640
+ LittleGhost.with_configuration(self) { Kernel.load(path) }
641
+ ensure
642
+ sealed_values = freeze_configuration_copy(@configuration_values)
643
+ @lifecycle_monitor.synchronize do
644
+ @configuration_values = sealed_values
645
+ @configuration_file_loading = false
646
+ end
647
+ end
648
+
649
+ def freeze_configuration_copy(value)
650
+ opaque_values = {}
651
+ copied = copy_configuration_value(value, {}, opaque_values)
652
+ freeze_configuration_value(copied, opaque_values)
653
+ end
654
+
655
+ def copy_configuration_value(value, ancestors = {}, opaque_values = {})
656
+ if value.is_a?(Hash) || value.is_a?(Array)
657
+ identity = value.object_id
658
+ raise ArgumentError, "cyclic values cannot be duplicated" if ancestors.key?(identity)
659
+
660
+ ancestors[identity] = true
661
+ end
662
+ case value
663
+ when Hash
664
+ supported_keys = true
665
+ Hash.instance_method(:each_key).bind_call(value) do |key|
666
+ supported_keys &&= configuration_declaration_key?(key)
667
+ end
668
+ unless supported_keys
669
+ opaque_values[value.object_id] = true
670
+ return value
671
+ end
672
+ copy = {}
673
+ Hash.instance_method(:each_pair).bind_call(value) do |key, child|
674
+ copy[copy_configuration_value(key, ancestors, opaque_values)] =
675
+ copy_configuration_value(child, ancestors, opaque_values)
676
+ end
677
+ copy
678
+ when Array
679
+ copy = []
680
+ Array.instance_method(:each).bind_call(value) do |child|
681
+ copy << copy_configuration_value(child, ancestors, opaque_values)
682
+ end
683
+ copy
684
+ when String
685
+ String.new(value)
686
+ else
687
+ value
688
+ end
689
+ ensure
690
+ ancestors.delete(identity) if identity
691
+ end
692
+
693
+ def configuration_declaration_key?(key)
694
+ key.instance_of?(String) ||
695
+ key.instance_of?(Symbol) ||
696
+ key.instance_of?(Integer) ||
697
+ key.instance_of?(Float) ||
698
+ key.instance_of?(Rational) ||
699
+ key.instance_of?(Complex) ||
700
+ key.nil? ||
701
+ key.equal?(true) ||
702
+ key.equal?(false)
703
+ end
704
+
705
+ def freeze_configuration_value(value, opaque_values = {})
706
+ return value if opaque_values.key?(value.object_id)
707
+
708
+ case value
709
+ when Hash
710
+ value.each do |key, child|
711
+ freeze_configuration_value(key, opaque_values)
712
+ freeze_configuration_value(child, opaque_values)
713
+ end
714
+ when Array
715
+ value.each { |child| freeze_configuration_value(child, opaque_values) }
716
+ end
717
+ value.freeze if value.is_a?(Hash) || value.is_a?(Array) || value.is_a?(String)
718
+ value
719
+ end
720
+
497
721
  def component_class(value, base_class, name)
498
722
  unless value.is_a?(Class) && value <= base_class
499
723
  raise ArgumentError, "#{name} must be a #{base_class} class"
@@ -37,7 +37,7 @@ module LittleGhost
37
37
  raise ArgumentError, "tool use name is required" if name.empty?
38
38
  raise ArgumentError, "tool use input must be an object" unless input.is_a?(Hash)
39
39
 
40
- super(id: id.to_s, name: name.to_s, input:)
40
+ super(id: id.to_s, name: name.to_s, input: DataMap.new(input))
41
41
  rescue TypeError
42
42
  raise ArgumentError, "tool use id and name must be strings"
43
43
  end
@@ -82,7 +82,7 @@ module LittleGhost
82
82
  text: text.freeze,
83
83
  signature: signature&.freeze,
84
84
  redacted_content: redacted_content&.freeze,
85
- details:
85
+ details: details&.map { |detail| DataMap.new(detail) }
86
86
  )
87
87
  rescue TypeError
88
88
  raise ArgumentError, "reasoning content is invalid"
@@ -166,7 +166,7 @@ module LittleGhost
166
166
 
167
167
  ##
168
168
  # :attr_reader: input
169
- # The object-shaped arguments supplied by the model.
169
+ # The object-shaped arguments supplied by the model as a DataMap.
170
170
  end
171
171
 
172
172
  # Carries the model-facing result for one ToolUse. A successful result uses
@@ -292,14 +292,14 @@ module LittleGhost
292
292
  value["data"] = Base64.strict_encode64(block.redacted_content)
293
293
  value["encoding"] = "base64"
294
294
  end
295
- value["details"] = block.details if block.details
295
+ value["details"] = block.details.map(&:to_h) if block.details
296
296
  end
297
297
  when Image
298
298
  binary("image", block.data, media_type: block.media_type)
299
299
  when Document
300
300
  binary("document", block.data, media_type: block.media_type, name: block.name)
301
301
  when ToolUse
302
- {"type" => "tool_use", "id" => block.id, "name" => block.name, "input" => block.input}
302
+ {"type" => "tool_use", "id" => block.id, "name" => block.name, "input" => block.input.to_h}
303
303
  when ToolResult
304
304
  {
305
305
  "type" => "tool_result", "tool_use_id" => block.tool_use_id,