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.
- checksums.yaml +4 -4
- data/README.md +68 -84
- data/docs/guides/assemblies.md +286 -0
- data/docs/guides/core_concepts.md +126 -231
- data/docs/guides/getting_started.md +114 -87
- data/docs/guides/production.md +187 -0
- data/docs/guides/prompt_views.md +132 -0
- data/lib/little_ghost/ag_ui/adapter.rb +3 -3
- data/lib/little_ghost/agent/delegation.rb +1 -1
- data/lib/little_ghost/agent.rb +167 -172
- data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
- data/lib/little_ghost/assembly.rb +55 -21
- data/lib/little_ghost/assembly_builder.rb +40 -2
- data/lib/little_ghost/assembly_execution.rb +87 -4
- data/lib/little_ghost/configuration.rb +263 -39
- data/lib/little_ghost/content.rb +5 -5
- data/lib/little_ghost/data_map.rb +209 -0
- data/lib/little_ghost/errors.rb +2 -2
- data/lib/little_ghost/execution.rb +32 -32
- data/lib/little_ghost/graph.rb +22 -3
- data/lib/little_ghost/message.rb +4 -4
- data/lib/little_ghost/model_resolver.rb +2 -2
- data/lib/little_ghost/prompt_resolver.rb +2 -0
- data/lib/little_ghost/run.rb +87 -49
- data/lib/little_ghost/run_context.rb +33 -20
- data/lib/little_ghost/runtime/hook.rb +3 -3
- data/lib/little_ghost/runtime.rb +71 -31
- data/lib/little_ghost/session.rb +12 -23
- data/lib/little_ghost/session_store.rb +9 -5
- data/lib/little_ghost/session_stores/agent_core_memory.rb +64 -56
- data/lib/little_ghost/session_stores/filesystem.rb +261 -0
- data/lib/little_ghost/session_stores/memory.rb +7 -0
- data/lib/little_ghost/subagents/manager.rb +42 -42
- data/lib/little_ghost/swarm.rb +13 -5
- data/lib/little_ghost/tool.rb +56 -14
- data/lib/little_ghost/tools/write_todos.rb +6 -1
- data/lib/little_ghost/tracing/open_telemetry.rb +1 -1
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost/workflow.rb +30 -21
- data/lib/little_ghost.rb +29 -25
- metadata +7 -2
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
|
|
5
5
|
module LittleGhost
|
|
6
|
-
class
|
|
6
|
+
class AgentInterjections # :nodoc: all
|
|
7
7
|
MAX_BATCH_SIZE = 100
|
|
8
|
-
|
|
8
|
+
MAX_INTERJECTION_COUNT = 1_000
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
def initialize(text:, tool_calls:,
|
|
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
|
-
|
|
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
|
|
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, "
|
|
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, "
|
|
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 >=
|
|
121
|
-
raise
|
|
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, "
|
|
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 =
|
|
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
|
|
115
|
-
attr_reader :run
|
|
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 ||
|
|
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
|
|
186
|
-
# <tt>preserve_context: true</tt> to
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
#
|
|
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[:
|
|
222
|
-
options[:
|
|
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
|
|
251
|
-
def
|
|
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
|
|
294
|
+
raise AgentInterjectionError, "Assembly is not currently running"
|
|
261
295
|
end
|
|
262
296
|
if active.length > 1
|
|
263
|
-
raise
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|