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,20 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "assembly"
4
+
3
5
  module LittleGhost
4
- # Build agentic workflows with ordinary Ruby branching and local variables.
5
- # A workflow composes several agents, consumes intermediate answers, and
6
- # streams one final agent response.
6
+ # Coordinates Assembly participants with ordinary Ruby control flow.
7
+ #
8
+ # A workflow is an Assembly whose +perform+ method controls ordering,
9
+ # branching, parallel work, and local variables. Each participant may be an
10
+ # Agent or another coordinated Assembly. The workflow consumes intermediate
11
+ # answers and streams one final participant response.
7
12
  #
8
- # A support workflow can route a difficult request through research before the
9
- # responder writes the caller-visible answer:
13
+ # A support workflow can guarantee that research happens before the responder
14
+ # writes the caller-visible answer:
10
15
  #
11
16
  # class ResponseWorkflow < LittleGhost::Workflow
12
17
  # private
13
18
  #
14
19
  # def perform
15
- # route = invoke(RouterAgent).output
16
- # return invoke(CustomerSupportAgent) unless route["research"]
17
- #
18
20
  # evidence = invoke(ResearchAgent).output
19
21
  # invoke CustomerSupportAgent, input: <<~PROMPT
20
22
  # #{input.text}
@@ -25,42 +27,45 @@ module LittleGhost
25
27
  # end
26
28
  # end
27
29
  #
28
- # run = runtime.build_run(
29
- # {message: "Why is transfer 481 pending?"},
30
- # agent_class: CustomerSupportAgent,
31
- # entrypoint_class: ResponseWorkflow
32
- # ).call
33
- # run.response # => "Transfer 481 is waiting for the receiving bank."
30
+ # run = ResponseWorkflow.ask("Why is transfer 481 pending?")
31
+ # run.response
32
+ # # One possible response: Transfer 481 is waiting for the receiving bank.
33
+ #
34
+ # Call a named Workflow with
35
+ # ask[rdoc-ref:LittleGhost::Assembly.ask] for its final Run, or
36
+ # the streaming entrypoint[rdoc-ref:LittleGhost::Assembly.stream_ask] for live
37
+ # events.
34
38
  #
35
39
  # +invoke+ returns a lazy Workflow::Invocation. Reading +output+ consumes an
36
40
  # intermediate invocation and returns RunResult#output; +perform+ must return
37
41
  # its final invocation without consuming it so those events reach the caller.
38
42
  # Intermediate usage is added to the final result.
39
43
  #
40
- # Every child inherits input, history, settings, cancellation, deadline,
41
- # template paths, and trace parentage unless +invoke+ overrides its input.
42
- # JSON-like context is copied for each child, preventing one intermediate agent
43
- # from mutating a sibling's state. Non-JSON-like workflow context raises
44
- # ArgumentError.
44
+ # A child receives the Workflow input unless +invoke+ supplies another one.
45
+ # It also inherits history, settings, cancellation, deadline, template paths,
46
+ # and the parent tracing relationship. JSON-like context is copied for each
47
+ # child, preventing one intermediate Agent from mutating a sibling's state.
48
+ # Non-JSON-like workflow context raises ArgumentError.
45
49
  #
46
- # A workflow instance streams once. Returning the wrong value, returning an
47
- # already consumed invocation, or consuming an invocation twice raises
48
- # ProtocolError. Built agents close in reverse order, and the first cleanup
49
- # failure is re-raised after every invocation has been given a chance to close.
50
- # Composition errors emit an +invocation_error+ event and then re-raise.
51
- class Workflow
52
- # Hold one lazy agent call inside a workflow composition.
50
+ # A Workflow instance streams once. Returning the wrong value, returning an
51
+ # already consumed invocation, or consuming one twice raises ProtocolError.
52
+ # A composition error fails the owning top-level Run. Each child Assembly
53
+ # closes after its attempt, and a cleanup failure raises from that attempt.
54
+ class Workflow < Assembly
55
+ # Hold one lazy Assembly call inside a workflow composition.
53
56
  # Workflow implementations normally use only its output method or return the
54
57
  # object as the final invocation.
55
58
  class Invocation
56
59
  attr_reader :result # :nodoc:
57
60
 
58
- def initialize(input:, history:, context:, build:, on_usage:) # :nodoc:
61
+ def initialize(reference:, participant:, input:, history:, context:, policies:, owner:) # :nodoc:
62
+ @reference = reference
63
+ @participant = participant
59
64
  @input = input
60
65
  @history = history
61
66
  @context = context
62
- @build = build
63
- @on_usage = on_usage
67
+ @policies = policies
68
+ @owner = owner
64
69
  @mutex = Mutex.new
65
70
  @consumed = false
66
71
  @closed = false
@@ -69,30 +74,29 @@ module LittleGhost
69
74
  def each(checkpoint: nil) # :nodoc:
70
75
  return enum_for(__method__, checkpoint:) unless block_given?
71
76
 
72
- agent, options = @mutex.synchronize do
77
+ @mutex.synchronize do
73
78
  raise Error, "workflow invocation is already closed" if @closed
74
79
  raise ProtocolError, "workflow invocation was already consumed" if @consumed
75
80
 
76
81
  @consumed = true
77
- @agent, options = @build.call
78
- [@agent, options]
79
82
  end
80
- begin
81
- agent.stream(
82
- @input,
83
- history: @history,
84
- context: @context,
85
- **options,
86
- checkpoint:
87
- ).each do |event|
88
- @result = event.data[:result] if event.type == :invocation_stop
89
- @usage = event.data[:usage] if event.type == :invocation_error
90
- yield event
91
- end
92
- ensure
93
- report_usage if @intermediate
94
- close
83
+ execution = @owner.send(
84
+ :execute_workflow_invocation,
85
+ reference: @reference,
86
+ participant: @participant,
87
+ input: @input,
88
+ history: @history,
89
+ context: @context,
90
+ policies: @policies,
91
+ checkpoint:
92
+ ) { |event| yield event }
93
+ @result = execution.result
94
+ @step = execution.step
95
+ @steps = execution.result.steps
96
+ execution.events.each do |event|
97
+ yield event
95
98
  end
99
+ @result
96
100
  end
97
101
 
98
102
  # Consumes this invocation when necessary and returns RunResult#output.
@@ -101,7 +105,12 @@ module LittleGhost
101
105
  # response text. Intermediate usage is recorded for the workflow total.
102
106
  def output
103
107
  @intermediate = true
104
- each {} unless consumed?
108
+ unless consumed?
109
+ each do |event|
110
+ @owner.send(:emit_workflow_event, event) if event.type.to_s.start_with?("assembly_")
111
+ end
112
+ @owner.send(:record_workflow_steps, @steps)
113
+ end
105
114
  result&.output
106
115
  end
107
116
 
@@ -110,29 +119,21 @@ module LittleGhost
110
119
  end
111
120
 
112
121
  def close # :nodoc:
113
- agent = @mutex.synchronize do
122
+ @mutex.synchronize do
114
123
  return if @closed
115
124
 
116
125
  @closed = true
117
- @agent
118
126
  end
119
- agent&.close
120
- end
121
-
122
- private
123
-
124
- def report_usage
125
- usage = result&.usage || @usage
126
- @on_usage.call(usage) if usage
127
127
  end
128
128
  end
129
129
 
130
- # Owning run and the runtime used to resolve workflow agents.
131
- attr_reader :run, :runtime
130
+ # Run that owns this run-scoped Workflow.
131
+ attr_reader :run
132
+ # Runtime used to resolve child Assemblies.
133
+ attr_reader :runtime
132
134
 
133
- def initialize(run:, runtime: run.runtime) # :nodoc:
134
- @run = run
135
- @runtime = runtime
135
+ def initialize(run: nil, runtime: nil) # :nodoc:
136
+ super(run:, runtime:, standalone: run.nil?)
136
137
  @mutex = Mutex.new
137
138
  @closed = false
138
139
  @started = false
@@ -162,6 +163,17 @@ module LittleGhost
162
163
  )
163
164
  raise ArgumentError, "input is required" if input.nil?
164
165
 
166
+ if standalone?
167
+ return build_run(entrypoint_payload(input, {
168
+ history:,
169
+ context:,
170
+ settings:,
171
+ template_paths:,
172
+ deadline_at: deadline,
173
+ cancellation_token:
174
+ }.compact)).each
175
+ end
176
+
165
177
  @mutex.synchronize do
166
178
  raise Error, "workflow is already closed" if @closed
167
179
  raise Error, "workflow instances can only be streamed once" if @started
@@ -178,11 +190,14 @@ module LittleGhost
178
190
  @parent_operation_id = parent_operation_id
179
191
  @checkpoint = checkpoint
180
192
  @intermediate_usage = Usage.new
193
+ @workflow_steps = []
194
+ @workflow_events = nil
181
195
  end
182
196
 
183
197
  Enumerator.new do |events|
184
198
  error_emitted = false
185
199
  observed_usage = nil
200
+ @workflow_events = events
186
201
  ensure_open!
187
202
  final_invocation = perform
188
203
  unless final_invocation.is_a?(Invocation) && !final_invocation.consumed?
@@ -197,6 +212,8 @@ module LittleGhost
197
212
  event.data.fetch(:result).usage
198
213
  when :invocation_error
199
214
  event.data[:usage] || observed_usage
215
+ when :assembly_step_error
216
+ event.data[:usage] || observed_usage
200
217
  else
201
218
  observed_usage
202
219
  end
@@ -212,10 +229,12 @@ module LittleGhost
212
229
  )
213
230
  end
214
231
  raise
232
+ ensure
233
+ @workflow_events = nil
215
234
  end
216
235
  end
217
236
 
218
- # Closes all built agent invocations in reverse order.
237
+ # Closes all declared invocations in reverse order.
219
238
  #
220
239
  # The operation is idempotent, attempts every close, and raises the first
221
240
  # cleanup failure.
@@ -250,35 +269,32 @@ module LittleGhost
250
269
  end
251
270
 
252
271
  # :doc:
253
- # Creates a lazy invocation for +agent_class_or_name+.
272
+ # Creates a lazy invocation for +assembly+.
254
273
  #
255
274
  # Intermediate calls may use +output+; the final call must be returned from
256
- # +perform+ without being consumed.
257
- def invoke(agent_class_or_name, input: self.input, history: self.history, context: self.context)
275
+ # +perform+ without being consumed. +as+ supplies the participant name used
276
+ # in steps and telemetry. Retries default to zero; a positive +retries+
277
+ # value requires explicit exception classes in +retry_on+.
278
+ def invoke(
279
+ assembly,
280
+ as: nil,
281
+ input: self.input,
282
+ history: self.history,
283
+ context: self.context,
284
+ timeout: nil,
285
+ retries: 0,
286
+ retry_on: nil,
287
+ retry_delay: 0
288
+ )
289
+ participant = as || assembly_identity(assembly)
258
290
  invocation = Invocation.new(
291
+ reference: assembly,
292
+ participant:,
259
293
  input:,
260
294
  history:,
261
295
  context: isolated_state(context),
262
- build: lambda {
263
- agent = runtime.build_agent(agent_class_or_name, run:)
264
- begin
265
- [
266
- agent,
267
- {
268
- cancellation_token: @cancellation_token,
269
- deadline: @deadline,
270
- settings: @settings,
271
- template_locals: template_locals_for(agent),
272
- template_paths: @template_paths,
273
- parent_operation_id: @parent_operation_id
274
- }
275
- ]
276
- rescue
277
- agent.close
278
- raise
279
- end
280
- },
281
- on_usage: ->(usage) { record_intermediate_usage(usage) }
296
+ policies: {timeout:, retries:, retry_on:, retry_delay:},
297
+ owner: self
282
298
  )
283
299
  @mutex.synchronize do
284
300
  raise Error, "workflow is already closed" if @closed
@@ -288,8 +304,116 @@ module LittleGhost
288
304
  invocation
289
305
  end
290
306
 
291
- def record_intermediate_usage(usage)
292
- @mutex.synchronize { @intermediate_usage += usage }
307
+ # :doc:
308
+ # Consumes independent invocations concurrently and returns their outputs in
309
+ # declaration order.
310
+ #
311
+ # +max_concurrency+ bounds active child executions. A child failure cancels
312
+ # siblings cooperatively before the error is raised.
313
+ def parallel(*invocations, max_concurrency: 8)
314
+ raise ArgumentError, "parallel requires at least one invocation" if invocations.empty?
315
+ unless invocations.all? { |invocation| invocation.is_a?(Invocation) && !invocation.consumed? }
316
+ raise ArgumentError, "parallel accepts unconsumed workflow invocations"
317
+ end
318
+
319
+ token = @cancellation_token.child
320
+ queue = SizedQueue.new(1_000)
321
+ worker = Thread.new do
322
+ results = Support::Executor.new(max_concurrency:).map(
323
+ invocations,
324
+ cancellation_token: token,
325
+ on_result: ->(_index, execution) { record_workflow_steps(execution.fetch(:steps)) }
326
+ ) do |invocation|
327
+ invocation.each do |event|
328
+ if event.type.to_s.start_with?("assembly_")
329
+ enqueue_assembly_event(queue, [:event, event], token)
330
+ end
331
+ end
332
+ {output: invocation.result&.output, steps: invocation.instance_variable_get(:@steps)}
333
+ end
334
+ enqueue_assembly_event(queue, [:done, results], token)
335
+ rescue => error
336
+ token.cancel
337
+ enqueue_assembly_terminal(queue, [:error, error])
338
+ end
339
+ executions = loop do
340
+ type, value = queue.pop
341
+ emit_workflow_event(value) if type == :event
342
+ raise value if type == :error
343
+ break value if type == :done
344
+ end
345
+ executions.map { |execution| execution.fetch(:output) }
346
+ ensure
347
+ token&.cancel
348
+ worker&.join
349
+ end
350
+
351
+ def execute_workflow_invocation(reference:, participant:, input:, history:, context:, policies:, checkpoint:)
352
+ step_id = SecureRandom.uuid
353
+ predecessor_id = @mutex.synchronize do
354
+ @workflow_steps.reverse.find { |step| step.parent_id.nil? }&.id
355
+ end
356
+ yield StreamEvent.build(
357
+ :assembly_step_start,
358
+ assembly_id: self.class.assembly_id,
359
+ assembly_kind: :workflow,
360
+ participant: participant.to_s,
361
+ step_id:
362
+ )
363
+ execution = execute_assembly_step(
364
+ reference:,
365
+ participant:,
366
+ input:,
367
+ history:,
368
+ context:,
369
+ cancellation_token: @cancellation_token,
370
+ deadline: @deadline,
371
+ settings: @settings,
372
+ template_locals: @template_locals,
373
+ template_paths: @template_paths,
374
+ parent_operation_id: @parent_operation_id,
375
+ policies:,
376
+ predecessor_ids: Array(predecessor_id),
377
+ checkpoint:,
378
+ step_id:
379
+ ) { |event| yield event }
380
+ yield StreamEvent.build(
381
+ :assembly_step_stop,
382
+ assembly_id: self.class.assembly_id,
383
+ assembly_kind: :workflow,
384
+ participant: participant.to_s,
385
+ step_id: execution.step.id,
386
+ usage: execution.step.usage
387
+ )
388
+ execution
389
+ end
390
+
391
+ def record_workflow_steps(steps)
392
+ @mutex.synchronize do
393
+ @workflow_steps.concat(steps)
394
+ @intermediate_usage += steps.first.usage
395
+ end
396
+ end
397
+
398
+ def emit_workflow_event(event)
399
+ sink = @mutex.synchronize do
400
+ if event.type == :assembly_step_error && event.data[:terminal] && event.data[:usage]
401
+ @intermediate_usage += event.data.fetch(:usage)
402
+ end
403
+ @workflow_events
404
+ end
405
+ sink << event if sink
406
+ end
407
+
408
+ def assembly_identity(reference)
409
+ case reference
410
+ when AssemblyBuilder, AssemblyDefinition
411
+ reference.assembly_id
412
+ when Class
413
+ (reference <= Assembly) ? reference.assembly_id : reference.to_s
414
+ else
415
+ reference.to_s
416
+ end
293
417
  end
294
418
 
295
419
  def aggregate_usage(event)
@@ -302,13 +426,19 @@ module LittleGhost
302
426
  usage: workflow_usage + result.usage,
303
427
  messages: result.messages,
304
428
  state: result.state,
305
- structured_result: result.structured_result
429
+ structured_result: result.structured_result,
430
+ steps: workflow_steps + result.steps
306
431
  )
307
432
  StreamEvent.build(event.type, **event.data.merge(result: combined))
308
433
  when :invocation_error
309
434
  usage = event.data[:usage]
310
435
  return event unless usage
311
436
 
437
+ StreamEvent.build(event.type, **event.data.merge(usage: workflow_usage + usage))
438
+ when :assembly_step_error
439
+ usage = event.data[:usage]
440
+ return event unless usage
441
+
312
442
  StreamEvent.build(event.type, **event.data.merge(usage: workflow_usage + usage))
313
443
  else
314
444
  event
@@ -338,6 +468,10 @@ module LittleGhost
338
468
  @mutex.synchronize { @intermediate_usage }
339
469
  end
340
470
 
471
+ def workflow_steps
472
+ @mutex.synchronize { @workflow_steps.dup.freeze }
473
+ end
474
+
341
475
  def ensure_open!
342
476
  @mutex.synchronize { raise Error, "workflow is already closed" if @closed }
343
477
  end
data/lib/little_ghost.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "little_ghost/version"
4
4
  require_relative "little_ghost/errors"
5
5
  require_relative "little_ghost/execution_state"
6
6
  require_relative "little_ghost/support"
7
+ require_relative "little_ghost/data_map"
7
8
  require_relative "little_ghost/events"
8
9
  require_relative "little_ghost/instrumentation"
9
10
  require_relative "little_ghost/tracing/open_telemetry"
@@ -50,52 +51,53 @@ require_relative "little_ghost/structured_output"
50
51
  require_relative "little_ghost/prompt_resolver"
51
52
  require_relative "little_ghost/session_store"
52
53
  require_relative "little_ghost/session_stores/memory"
54
+ require_relative "little_ghost/session_stores/filesystem"
53
55
  require_relative "little_ghost/session"
54
56
  require_relative "little_ghost/skills"
55
57
  require_relative "little_ghost/tools/write_todos"
56
58
  require_relative "little_ghost/run"
59
+ require_relative "little_ghost/execution"
57
60
  require_relative "little_ghost/subagents/definition"
58
61
  require_relative "little_ghost/subagents/agent_path"
59
62
  require_relative "little_ghost/subagents/manager"
60
- require_relative "little_ghost/agent_interruptions"
63
+ require_relative "little_ghost/agent_interjections"
64
+ require_relative "little_ghost/assembly"
65
+ require_relative "little_ghost/assembly_execution"
61
66
  require_relative "little_ghost/agent"
62
- require_relative "little_ghost/agent_builder"
63
67
  require_relative "little_ghost/workflow"
68
+ require_relative "little_ghost/swarm"
69
+ require_relative "little_ghost/graph"
70
+ require_relative "little_ghost/assembly_builder"
71
+ require_relative "little_ghost/agent_factory"
64
72
  require_relative "little_ghost/runtime/hook"
65
73
  require_relative "little_ghost/runtime"
66
74
 
67
- # Build AI features with reusable agents and agentic workflows. LittleGhost can
68
- # sit inside an existing Ruby system or support a dedicated AI service, keeping
69
- # models, prompts, tools, sessions, streaming, and instrumentation behind a
70
- # cohesive set of Ruby conventions.
75
+ # LittleGhost is a Ruby library for building AI features with reusable agents
76
+ # and composable assemblies. It can sit inside an existing Ruby system or
77
+ # support a dedicated AI service.
71
78
  #
72
- # A customer support agent can answer directly, use an application tool, or ask a
73
- # specialist for research while the caller observes one run:
74
- #
75
- # class ResearchAgent < LittleGhost::Agent
76
- # description "Researches difficult support questions"
77
- # model "openai:gpt-5.6-luna"
78
- # end
79
+ # Start with one model-driven behavior:
79
80
  #
80
81
  # class CustomerSupportAgent < LittleGhost::Agent
81
82
  # description "Handles support requests"
82
- # model "openai:gpt-5.6-luna"
83
- # tools AccountTool
84
- # subagent ResearchAgent, kind: "research"
83
+ # model "openrouter:openai/gpt-5.6-luna"
84
+ # system_prompt "Answer customer questions clearly."
85
85
  # end
86
86
  #
87
87
  # run = CustomerSupportAgent.ask("Why is transfer 481 still pending?")
88
88
  # run.completed? # => true
89
- # run.response # => "Transfer 481 is waiting for the receiving bank."
89
+ # run.response
90
+ # # One possible response: Transfer 481 is waiting for the receiving bank.
90
91
  #
91
- # Agent subclasses hold reusable behavior; Invocation objects carry one request,
92
- # and Run objects own execution and cleanup. Workflow subclasses can compose
93
- # several agents when ordinary Ruby branching is clearer than one agent loop.
92
+ # Agent subclasses hold reusable behavior, Invocation objects carry one request,
93
+ # and Run objects own execution and cleanup. When one model loop is not enough,
94
+ # Assembly gives Agent, Workflow, Swarm, and Graph the same caller interface
95
+ # while each type owns a different coordination policy.
94
96
  #
95
97
  # LittleGhost.configuration is process-wide unless LittleGhost.with_configuration
96
- # supplies an execution-scoped replacement. Configuration is loaded lazily and
97
- # snapshotted into a Runtime, so make application changes before the first agent
98
- # runtime is built.
98
+ # supplies an execution-scoped replacement. The first standalone call lazily
99
+ # builds one shared Runtime from that configuration, so make application changes
100
+ # before invoking an Agent or Assembly.
99
101
  module LittleGhost
100
102
  class << self
101
103
  # The configuration active in the current execution context, falling back to
@@ -107,15 +109,24 @@ module LittleGhost
107
109
  # Opens the active Configuration for application setup and returns it.
108
110
  #
109
111
  # Configuration files are loaded lazily when a runtime is first built, so
110
- # make application-level changes before invoking an agent.
112
+ # make application-level changes before invoking an agent. Once the shared
113
+ # Runtime is ready, later mutations raise ConfigurationError.
111
114
  def configure(&block)
112
115
  configuration.configure(&block)
113
116
  end
114
117
 
118
+ # Returns the shared Runtime for the active Configuration.
119
+ #
120
+ # Most applications do not need to call this method. Standalone Agent and
121
+ # Assembly entrypoints use it automatically. Runtime construction is lazy,
122
+ # thread-safe, and locks the active Configuration after it succeeds.
123
+ def runtime = configuration.runtime
124
+
115
125
  # Returns the model resolver owned by the active process configuration.
116
126
  def model_resolver = configuration.model_resolver
117
127
 
118
- # Makes +configuration+ current only while the block runs.
128
+ # Makes +configuration+ and its independent shared Runtime current only
129
+ # while the block runs.
119
130
  #
120
131
  # Execution state restores the previous configuration even when the block
121
132
  # raises. Other execution contexts continue to see their own configuration.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: little_ghost
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Robinson
@@ -107,8 +107,8 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '1.9'
110
- description: Add agents, tools, and agentic workflows to existing Ruby systems or
111
- dedicated AI services.
110
+ description: Add agents, tools, workflows, swarms, and graphs to existing Ruby systems
111
+ or dedicated AI services.
112
112
  email:
113
113
  - robinson.matty@gmail.com
114
114
  executables: []
@@ -117,8 +117,11 @@ extra_rdoc_files: []
117
117
  files:
118
118
  - LICENSE.txt
119
119
  - README.md
120
+ - docs/guides/assemblies.md
120
121
  - docs/guides/core_concepts.md
121
122
  - docs/guides/getting_started.md
123
+ - docs/guides/production.md
124
+ - docs/guides/prompt_views.md
122
125
  - lib/little_ghost.rb
123
126
  - lib/little_ghost/ag_ui.rb
124
127
  - lib/little_ghost/ag_ui/adapter.rb
@@ -128,13 +131,20 @@ files:
128
131
  - lib/little_ghost/agent/skills.rb
129
132
  - lib/little_ghost/agent/tool_loop.rb
130
133
  - lib/little_ghost/agent_builder.rb
131
- - lib/little_ghost/agent_interruptions.rb
134
+ - lib/little_ghost/agent_factory.rb
135
+ - lib/little_ghost/agent_interjections.rb
136
+ - lib/little_ghost/assembly.rb
137
+ - lib/little_ghost/assembly_builder.rb
138
+ - lib/little_ghost/assembly_execution.rb
132
139
  - lib/little_ghost/configuration.rb
133
140
  - lib/little_ghost/content.rb
134
141
  - lib/little_ghost/data/model_catalog.json
142
+ - lib/little_ghost/data_map.rb
135
143
  - lib/little_ghost/errors.rb
136
144
  - lib/little_ghost/events.rb
145
+ - lib/little_ghost/execution.rb
137
146
  - lib/little_ghost/execution_state.rb
147
+ - lib/little_ghost/graph.rb
138
148
  - lib/little_ghost/instrumentation.rb
139
149
  - lib/little_ghost/invocation.rb
140
150
  - lib/little_ghost/lookup.rb
@@ -182,6 +192,7 @@ files:
182
192
  - lib/little_ghost/session.rb
183
193
  - lib/little_ghost/session_store.rb
184
194
  - lib/little_ghost/session_stores/agent_core_memory.rb
195
+ - lib/little_ghost/session_stores/filesystem.rb
185
196
  - lib/little_ghost/session_stores/memory.rb
186
197
  - lib/little_ghost/skills.rb
187
198
  - lib/little_ghost/skills/catalog.rb
@@ -203,6 +214,7 @@ files:
203
214
  - lib/little_ghost/support/output_truncation.rb
204
215
  - lib/little_ghost/support/redactor.rb
205
216
  - lib/little_ghost/support/sse_parser.rb
217
+ - lib/little_ghost/swarm.rb
206
218
  - lib/little_ghost/tool.rb
207
219
  - lib/little_ghost/tool_execution.rb
208
220
  - lib/little_ghost/tool_registry.rb
@@ -242,5 +254,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
242
254
  requirements: []
243
255
  rubygems_version: 4.0.10
244
256
  specification_version: 4
245
- summary: A Ruby framework for AI features with agents and agentic workflows
257
+ summary: A Ruby framework for AI features with agents and composable assemblies
246
258
  test_files: []