little_ghost 0.3.0 → 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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +68 -84
  3. data/docs/guides/assemblies.md +286 -0
  4. data/docs/guides/core_concepts.md +126 -231
  5. data/docs/guides/getting_started.md +114 -87
  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 +1 -1
  10. data/lib/little_ghost/agent.rb +167 -172
  11. data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
  12. data/lib/little_ghost/assembly.rb +55 -21
  13. data/lib/little_ghost/assembly_builder.rb +40 -2
  14. data/lib/little_ghost/assembly_execution.rb +87 -4
  15. data/lib/little_ghost/configuration.rb +263 -39
  16. data/lib/little_ghost/content.rb +5 -5
  17. data/lib/little_ghost/data_map.rb +209 -0
  18. data/lib/little_ghost/errors.rb +2 -2
  19. data/lib/little_ghost/execution.rb +32 -32
  20. data/lib/little_ghost/graph.rb +22 -3
  21. data/lib/little_ghost/message.rb +4 -4
  22. data/lib/little_ghost/model_resolver.rb +2 -2
  23. data/lib/little_ghost/prompt_resolver.rb +2 -0
  24. data/lib/little_ghost/run.rb +87 -49
  25. data/lib/little_ghost/run_context.rb +33 -20
  26. data/lib/little_ghost/runtime/hook.rb +3 -3
  27. data/lib/little_ghost/runtime.rb +71 -31
  28. data/lib/little_ghost/session.rb +12 -23
  29. data/lib/little_ghost/session_store.rb +9 -5
  30. data/lib/little_ghost/session_stores/agent_core_memory.rb +64 -56
  31. data/lib/little_ghost/session_stores/filesystem.rb +261 -0
  32. data/lib/little_ghost/session_stores/memory.rb +7 -0
  33. data/lib/little_ghost/subagents/manager.rb +42 -42
  34. data/lib/little_ghost/swarm.rb +13 -5
  35. data/lib/little_ghost/tool.rb +56 -14
  36. data/lib/little_ghost/tools/write_todos.rb +6 -1
  37. data/lib/little_ghost/tracing/open_telemetry.rb +1 -1
  38. data/lib/little_ghost/version.rb +1 -1
  39. data/lib/little_ghost/workflow.rb +30 -21
  40. data/lib/little_ghost.rb +29 -25
  41. metadata +7 -2
@@ -3,16 +3,16 @@
3
3
  require "securerandom"
4
4
 
5
5
  module LittleGhost
6
- class AgentInterruptions # :nodoc: all
6
+ class AgentInterjections # :nodoc: all
7
7
  MAX_BATCH_SIZE = 100
8
- MAX_INTERRUPTION_COUNT = 1_000
8
+ MAX_INTERJECTION_COUNT = 1_000
9
9
 
10
- Response = Data.define(:text, :tool_calls, :interruption_ids, :batch_key) do
11
- def initialize(text:, tool_calls:, interruption_ids: [], batch_key: nil)
10
+ Result = Data.define(:text, :tool_calls, :interjection_ids, :batch_key) do
11
+ def initialize(text:, tool_calls:, interjection_ids: [], batch_key: nil)
12
12
  super(
13
13
  text: String(text),
14
14
  tool_calls: !!tool_calls,
15
- interruption_ids: Array(interruption_ids).map { |id| String(id).dup.freeze }.freeze,
15
+ interjection_ids: Array(interjection_ids).map { |id| String(id).dup.freeze }.freeze,
16
16
  batch_key: batch_key.nil? ? nil : String(batch_key).dup.freeze
17
17
  )
18
18
  end
@@ -21,7 +21,7 @@ module LittleGhost
21
21
  end
22
22
 
23
23
  Batch = Data.define(:tickets) do
24
- def interruption_ids = tickets.map(&:id)
24
+ def interjection_ids = tickets.map(&:id)
25
25
  def batch_key = tickets.first&.batch_key
26
26
  def metadata = tickets.last&.metadata
27
27
  end
@@ -99,7 +99,7 @@ module LittleGhost
99
99
 
100
100
  def enqueue(message, id: SecureRandom.uuid, batch_key: nil, metadata: {})
101
101
  id = String(id)
102
- raise ArgumentError, "interruption_id cannot be empty" if id.empty?
102
+ raise ArgumentError, "interjection_id cannot be empty" if id.empty?
103
103
 
104
104
  batch_key = String(batch_key) unless batch_key.nil?
105
105
  metadata = metadata.to_h
@@ -111,14 +111,14 @@ module LittleGhost
111
111
  unless existing.message.to_h == message.to_h &&
112
112
  existing.batch_key == batch_key &&
113
113
  existing.metadata == metadata
114
- raise ArgumentError, "interruption_id has already been used with different input"
114
+ raise ArgumentError, "interjection_id has already been used with different input"
115
115
  end
116
116
 
117
117
  @waiters[existing] += 1
118
118
  return existing
119
119
  end
120
- if @tickets_by_id.length >= MAX_INTERRUPTION_COUNT
121
- raise AgentInterruptError, "Agent interruption capacity reached"
120
+ if @tickets_by_id.length >= MAX_INTERJECTION_COUNT
121
+ raise AgentInterjectionError, "Agent interjection capacity reached"
122
122
  end
123
123
 
124
124
  ticket = Ticket.new(message, id: id.freeze, batch_key: batch_key&.freeze, metadata:)
@@ -128,7 +128,7 @@ module LittleGhost
128
128
  ticket
129
129
  end
130
130
  rescue TypeError, NoMethodError
131
- raise ArgumentError, "interruption_id, batch_key, and metadata are invalid"
131
+ raise ArgumentError, "interjection_id, batch_key, and metadata are invalid"
132
132
  end
133
133
 
134
134
  def deliver
@@ -166,7 +166,7 @@ module LittleGhost
166
166
  return true if @closed_error
167
167
  return false if @delivered || !@queue.empty?
168
168
 
169
- @closed_error = AgentInterruptError.new("Agent is not currently running")
169
+ @closed_error = AgentInterjectionError.new("Agent is not currently running")
170
170
  true
171
171
  end
172
172
  end
@@ -14,10 +14,28 @@ module LittleGhost
14
14
  # agent_run.response
15
15
  # graph_run.response
16
16
  #
17
+ # Named subclasses are the usual form. +to_builder+ creates a mutable dynamic
18
+ # definition seeded by the class, while +definition+ returns the immutable
19
+ # snapshot used for one execution. Composite results expose Assembly::Step
20
+ # records through RunResult#trajectory.
21
+ #
17
22
  # A standalone assembly owns a top-level Run. An assembly built by a Runtime
18
23
  # participates in the existing run and returns a RunResult. Applications
19
24
  # normally subclass Agent, Workflow, Swarm, or Graph rather than Assembly
20
- # directly.
25
+ # directly. Standalone calls automatically reuse the active Configuration's
26
+ # shared Runtime while keeping each Run and its resources independent.
27
+ #
28
+ # == What each calling form returns
29
+ #
30
+ # [<tt>CustomerSupportAgent.ask(...)</tt>]
31
+ # A named class creates and returns a top-level Run.
32
+ # [<tt>CustomerSupportAgent.new(runtime: runtime).ask(...)</tt>]
33
+ # A standalone instance also creates and returns a top-level Run.
34
+ # [<tt>runtime.build_assembly(..., run: run).call(...)</tt>]
35
+ # A participant already bound to a Run returns its child RunResult.
36
+ # [<tt>stream_ask(...).each { |event| ... }</tt>]
37
+ # A standalone stream returns its top-level Run after enumeration. A
38
+ # run-scoped stream ends with an +invocation_stop+ event carrying RunResult.
21
39
  class Assembly
22
40
  extend Support::ClassAttributes
23
41
 
@@ -26,11 +44,18 @@ module LittleGhost
26
44
 
27
45
  class << self
28
46
  # Executes +message+ through a fresh standalone assembly and returns its Run.
47
+ #
48
+ # +options+ become Invocation fields. Common values include +history+,
49
+ # +context+, +settings+, +metadata+, +session_id+, +actor_id+, and
50
+ # +deadline_at+.
29
51
  def ask(message, **options)
30
52
  definition.implementation.new.ask(message, **options)
31
53
  end
32
54
 
33
55
  # Lazily streams +message+ through a fresh standalone assembly.
56
+ #
57
+ # Enumeration yields StreamEvent objects and returns the terminal Run.
58
+ # The same Invocation fields accepted by .ask may be supplied as +options+.
34
59
  def stream_ask(message, **options)
35
60
  snapshot = definition
36
61
  stream = nil
@@ -111,12 +136,18 @@ module LittleGhost
111
136
  end
112
137
  end
113
138
 
114
- # The owning run, runtime, and optional standalone resources.
115
- attr_reader :run, :runtime, :workspace, :sandbox
139
+ # The owning Run, or +nil+ for a standalone entrypoint.
140
+ attr_reader :run
141
+ # Runtime used to resolve participants and build Runs.
142
+ attr_reader :runtime
143
+ # Workspace supplied to this Assembly, when present.
144
+ attr_reader :workspace
145
+ # Sandbox supplied to this Assembly, when present.
146
+ attr_reader :sandbox
116
147
 
117
148
  def initialize(run: nil, runtime: nil, workspace: nil, sandbox: nil, standalone: run.nil?) # :nodoc:
118
149
  @run = run
119
- @runtime = runtime || run&.runtime || Runtime.new(configuration: LittleGhost.configuration)
150
+ @runtime = runtime || run&.runtime || LittleGhost.runtime
120
151
  @workspace = workspace || (run.workspace if run&.respond_to?(:workspace))
121
152
  @sandbox = sandbox || (run.sandbox if run&.respond_to?(:sandbox))
122
153
  @standalone = standalone
@@ -162,11 +193,18 @@ module LittleGhost
162
193
  end
163
194
 
164
195
  # Runs +message+ to completion.
196
+ #
197
+ # A standalone instance returns its owning Run. A run-scoped instance
198
+ # returns the child RunResult.
165
199
  def ask(message, **options)
166
200
  call(message, **options)
167
201
  end
168
202
 
169
203
  # Lazily streams +message+ through the standalone or run-scoped assembly.
204
+ #
205
+ # A standalone stream returns its terminal Run after enumeration. A
206
+ # run-scoped stream finishes with an +invocation_stop+ event containing its
207
+ # RunResult.
170
208
  def stream_ask(message, **options)
171
209
  if standalone?
172
210
  options[:deadline_at] = options.delete(:deadline) if options.key?(:deadline)
@@ -182,11 +220,12 @@ module LittleGhost
182
220
 
183
221
  # Exposes this assembly as a Tool instance.
184
222
  #
185
- # By default each call has empty conversational history. Set
186
- # <tt>preserve_context: true</tt> to retain history serially. Every call still
187
- # receives the invoking Tool::Context application state; +preserve_context+
188
- # does not suppress it. Tools inside the assembly remain responsible for
189
- # authorizing privileged work from trusted context.
223
+ # By default, calls do not remember earlier conversation history. Set
224
+ # <tt>preserve_context: true</tt> to carry that history from one tool call to
225
+ # the next. This option does not control working state: every call receives
226
+ # the invoking Tool's current RunContext#state, which may include current
227
+ # request values or values restored from a Session. Nested tools must still
228
+ # authorize privileged work with current, application-established values.
190
229
  def as_tool(name: self.class.assembly_id, description: self.class.description, preserve_context: false)
191
230
  assembly = self
192
231
  description = "Delegate a task to #{name}." if description.to_s.empty?
@@ -218,8 +257,8 @@ module LittleGhost
218
257
  parent_operation_id: assembly.run&.operation_id
219
258
  }
220
259
  if target.is_a?(Agent)
221
- options[:interruption_metadata] = context&.interruption_metadata
222
- options[:interruption_ids] = context&.interruption_ids || []
260
+ options[:interjection_metadata] = context&.interjection_metadata
261
+ options[:interjection_ids] = context&.interjection_ids || []
223
262
  end
224
263
  result = target.call(input.fetch("input"), **options)
225
264
  if result.is_a?(Run)
@@ -247,24 +286,19 @@ module LittleGhost
247
286
  ))
248
287
  end
249
288
 
250
- # Adds +message+ to the single active leaf Agent and returns its text reply.
251
- def interrupt(message, **options)
252
- interrupt_response(message, **options).text
253
- end
254
-
255
- # Adds an interruption to the single active leaf Agent.
256
- def interrupt_response(message, **options)
289
+ # Adds an interjection to the single active leaf Agent.
290
+ def interject(message, **options)
257
291
  child = @assembly_mutex.synchronize do
258
292
  active = @active_assemblies.dup
259
293
  if active.empty?
260
- raise AgentInterruptError, "Assembly is not currently running"
294
+ raise AgentInterjectionError, "Assembly is not currently running"
261
295
  end
262
296
  if active.length > 1
263
- raise AgentInterruptError, "Assembly has multiple active participants; the interruption target is ambiguous"
297
+ raise AgentInterjectionError, "Assembly has multiple active participants; the interjection target is ambiguous"
264
298
  end
265
299
  active.first
266
300
  end
267
- child.interrupt_response(message, **options)
301
+ child.interject(message, **options)
268
302
  end
269
303
 
270
304
  # Closes resources owned directly by this assembly.
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LittleGhost
4
- # An immutable, executable snapshot produced by an AssemblyBuilder.
5
- AssemblyDefinition = Data.define(:kind, :assembly_id, :description, :implementation) do
4
+ AssemblyDefinition = Data.define(:kind, :assembly_id, :description, :implementation) do # :nodoc:
6
5
  def initialize(kind:, assembly_id:, description:, implementation:)
7
6
  super(
8
7
  kind: kind.to_sym,
@@ -13,6 +12,29 @@ module LittleGhost
13
12
  end
14
13
  end
15
14
 
15
+ # The immutable, executable snapshot produced by an AssemblyBuilder.
16
+ #
17
+ # Runtime and other builders accept a definition anywhere they accept an
18
+ # Assembly reference. +implementation+ is the sealed class instantiated for
19
+ # this snapshot.
20
+ class AssemblyDefinition < Data # :doc:
21
+ ##
22
+ # :attr_reader: kind
23
+ # The Assembly kind: +:agent+, +:workflow+, +:swarm+, or +:graph+.
24
+
25
+ ##
26
+ # :attr_reader: assembly_id
27
+ # The stable identifier captured by the snapshot.
28
+
29
+ ##
30
+ # :attr_reader: description
31
+ # The human-readable description captured by the snapshot.
32
+
33
+ ##
34
+ # :attr_reader: implementation
35
+ # The sealed Assembly subclass used to build executions.
36
+ end
37
+
16
38
  # Builds an Assembly when its participants or routes are discovered at runtime.
17
39
  #
18
40
  # Class definitions are the usual, easier-to-find way to declare behavior.
@@ -230,6 +252,12 @@ module LittleGhost
230
252
  end
231
253
 
232
254
  # Builds an Agent definition from declarations made at runtime.
255
+ #
256
+ # agent = LittleGhost::AgentBuilder.new(id: "customer_support")
257
+ # agent.model "openrouter:openai/gpt-5.6-luna"
258
+ # agent.system_prompt "Answer customer questions clearly."
259
+ # agent.ask("Where is my order?")
260
+ #
233
261
  # Supported Agent class-DSL calls are recorded and replayed into each
234
262
  # immutable snapshot.
235
263
  class AgentBuilder < AssemblyBuilder
@@ -308,6 +336,9 @@ module LittleGhost
308
336
  end
309
337
 
310
338
  # Builds a Workflow whose Ruby composition block is supplied at runtime.
339
+ # Use a named Workflow subclass's +to_builder+ when the class supplies the
340
+ # composition and runtime configuration supplies its identity or description.
341
+ # +perform+ supplies the underlying dynamic form for trusted application code.
311
342
  class WorkflowBuilder < AssemblyBuilder
312
343
  class << self
313
344
  # :nodoc:
@@ -344,6 +375,13 @@ module LittleGhost
344
375
  end
345
376
 
346
377
  # Builds a Swarm at runtime while keeping its members Agent-only.
378
+ #
379
+ # swarm = LittleGhost::SwarmBuilder.new(id: "problem_solver")
380
+ # swarm.member TriageAgent
381
+ # swarm.member BillingAgent
382
+ # swarm.start TriageAgent
383
+ # swarm.handoff TriageAgent, to: BillingAgent
384
+ # swarm.validate!
347
385
  class SwarmBuilder < AssemblyBuilder
348
386
  class << self
349
387
  # :nodoc:
@@ -8,15 +8,13 @@ module LittleGhost
8
8
  MAX_STEP_EVENTS = 10_000 # :nodoc:
9
9
  MAX_STEP_EVENT_BYTES = 10 * 1024 * 1024 # :nodoc:
10
10
 
11
- # One attempt to execute an assembly step.
12
- Attempt = Data.define(:number, :status, :started_at, :finished_at, :usage, :error) do
11
+ Attempt = Data.define(:number, :status, :started_at, :finished_at, :usage, :error) do # :nodoc:
13
12
  def initialize(number:, status:, started_at:, finished_at:, usage: Usage.new, error: nil)
14
13
  super(number:, status: status.to_sym, started_at:, finished_at:, usage:, error: error&.to_s&.freeze)
15
14
  end
16
15
  end
17
16
 
18
- # One logical child execution in a composite assembly.
19
- Step = Data.define(
17
+ Step = Data.define( # :nodoc:
20
18
  :id, :parent_id, :predecessor_ids, :branch_id, :participant,
21
19
  :assembly_id, :assembly_kind, :status, :attempts, :usage, :output, :output_truncated
22
20
  ) do
@@ -39,6 +37,91 @@ module LittleGhost
39
37
  end
40
38
  end
41
39
 
40
+ # One bounded attempt to execute a child Assembly step.
41
+ #
42
+ # It records timing, normalized usage, terminal status, and a sanitized error
43
+ # description suitable for the public coordination trajectory.
44
+ class Attempt < Data # :doc:
45
+ ##
46
+ # :attr_reader: number
47
+ # The one-based attempt number.
48
+
49
+ ##
50
+ # :attr_reader: status
51
+ # The normalized terminal status for this attempt.
52
+
53
+ ##
54
+ # :attr_reader: started_at
55
+ # The wall-clock start time.
56
+
57
+ ##
58
+ # :attr_reader: finished_at
59
+ # The wall-clock finish time.
60
+
61
+ ##
62
+ # :attr_reader: usage
63
+ # The Usage recorded by this attempt.
64
+
65
+ ##
66
+ # :attr_reader: error
67
+ # A sanitized error description, or +nil+.
68
+ end
69
+
70
+ # One logical child execution in a composite Assembly result.
71
+ #
72
+ # Steps identify the participant, relationships to other steps, attempts,
73
+ # usage, and a bounded semantic output. Use RunResult#trajectory for queries
74
+ # over several steps.
75
+ class Step < Data # :doc:
76
+ ##
77
+ # :attr_reader: id
78
+ # The stable identifier for this step occurrence.
79
+
80
+ ##
81
+ # :attr_reader: parent_id
82
+ # The containing step identifier for nested coordination, or +nil+.
83
+
84
+ ##
85
+ # :attr_reader: predecessor_ids
86
+ # Step identifiers whose results led to this step.
87
+
88
+ ##
89
+ # :attr_reader: branch_id
90
+ # The parallel branch identifier, or +nil+.
91
+
92
+ ##
93
+ # :attr_reader: participant
94
+ # The participant name used by the parent Assembly.
95
+
96
+ ##
97
+ # :attr_reader: assembly_id
98
+ # The invoked Assembly's stable identifier.
99
+
100
+ ##
101
+ # :attr_reader: assembly_kind
102
+ # The invoked Assembly kind.
103
+
104
+ ##
105
+ # :attr_reader: status
106
+ # The logical step's terminal status.
107
+
108
+ ##
109
+ # :attr_reader: attempts
110
+ # Immutable Attempt values, including retries.
111
+
112
+ ##
113
+ # :attr_reader: usage
114
+ # Usage accumulated across the step's attempts.
115
+
116
+ ##
117
+ # :attr_reader: output
118
+ # The bounded semantic output retained for coordination inspection.
119
+
120
+ ##
121
+ # :attr_reader: output_truncated
122
+ # Indicates that +output+ exceeded the public result limit.
123
+ end
124
+
42
125
  # Immutable queries over the steps returned by one assembly invocation.
43
126
  class Trajectory
44
127
  include Enumerable