little_ghost 0.2.0 → 0.3.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 +19 -5
- data/docs/guides/core_concepts.md +167 -38
- data/docs/guides/getting_started.md +8 -4
- data/lib/little_ghost/agent/delegation.rb +34 -7
- data/lib/little_ghost/agent/tool_loop.rb +2 -1
- data/lib/little_ghost/agent.rb +115 -156
- data/lib/little_ghost/agent_builder.rb +20 -4
- data/lib/little_ghost/agent_factory.rb +3 -0
- data/lib/little_ghost/assembly.rb +311 -0
- data/lib/little_ghost/assembly_builder.rb +459 -0
- data/lib/little_ghost/assembly_execution.rb +452 -0
- data/lib/little_ghost/errors.rb +8 -0
- data/lib/little_ghost/execution.rb +206 -0
- data/lib/little_ghost/graph.rb +911 -0
- data/lib/little_ghost/run.rb +120 -32
- data/lib/little_ghost/run_result.rb +22 -11
- data/lib/little_ghost/runtime/hook.rb +7 -2
- data/lib/little_ghost/runtime.rb +64 -6
- data/lib/little_ghost/sandbox.rb +1 -1
- data/lib/little_ghost/support/executor.rb +14 -2
- data/lib/little_ghost/support/loader.rb +2 -2
- data/lib/little_ghost/support.rb +15 -3
- data/lib/little_ghost/swarm.rb +431 -0
- data/lib/little_ghost/tool.rb +32 -6
- data/lib/little_ghost/tracing/open_telemetry.rb +13 -2
- data/lib/little_ghost/unrestricted_sandbox.rb +1 -1
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost/workflow.rb +198 -73
- data/lib/little_ghost.rb +11 -4
- metadata +11 -4
data/lib/little_ghost/run.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
|
|
5
5
|
module LittleGhost
|
|
6
|
-
# Observe one top-level
|
|
6
|
+
# Observe one top-level assembly execution from start to finish.
|
|
7
7
|
# A run records its response, outcome, usage, error, and owned resources.
|
|
8
8
|
#
|
|
9
9
|
# run = CustomerSupportAgent.ask("Why is transfer 481 pending?")
|
|
@@ -12,11 +12,11 @@ module LittleGhost
|
|
|
12
12
|
# run.outcome # => "completed"
|
|
13
13
|
# run.response # => "Transfer 481 is waiting for the receiving bank."
|
|
14
14
|
#
|
|
15
|
-
# The class-level ask helper[rdoc-ref:LittleGhost::
|
|
16
|
-
# ask method[rdoc-ref:LittleGhost::
|
|
15
|
+
# The class-level ask helper[rdoc-ref:LittleGhost::Assembly.ask] or standalone
|
|
16
|
+
# ask method[rdoc-ref:LittleGhost::Assembly#ask] consumes the event stream and
|
|
17
17
|
# returns the Run. For a live interface, the class-level streaming
|
|
18
|
-
# helper[rdoc-ref:LittleGhost::
|
|
19
|
-
# method[rdoc-ref:LittleGhost::
|
|
18
|
+
# helper[rdoc-ref:LittleGhost::Assembly.stream_ask] or standalone streaming
|
|
19
|
+
# method[rdoc-ref:LittleGhost::Assembly#stream_ask] yields StreamEvent objects
|
|
20
20
|
# and returns the same run after enumeration. A run can execute only once.
|
|
21
21
|
#
|
|
22
22
|
# Completion, failure, deadline, and cancellation become the +completed+,
|
|
@@ -25,7 +25,7 @@ module LittleGhost
|
|
|
25
25
|
# delivery, or instrumentation failures may still raise because the framework
|
|
26
26
|
# cannot safely report a clean stop.
|
|
27
27
|
#
|
|
28
|
-
# The run opens its workspace, sandbox, session, and entrypoint, then closes
|
|
28
|
+
# The run opens its workspace, sandbox, session, and assembly entrypoint, then closes
|
|
29
29
|
# registered resources in reverse order. +register+ extends that lifecycle for
|
|
30
30
|
# application resources. Interruption is available only while an agent
|
|
31
31
|
# entrypoint is active and unambiguous.
|
|
@@ -38,11 +38,17 @@ module LittleGhost
|
|
|
38
38
|
:outcome, :response, :error, :session, :usage, :workspace, :sandbox
|
|
39
39
|
|
|
40
40
|
# Creates a dormant run for +invocation+.
|
|
41
|
-
def initialize(invocation:,
|
|
41
|
+
def initialize(invocation:, runtime:, agent_class: nil, assembly_class: nil, entrypoint_class: nil,
|
|
42
|
+
execution_class: nil,
|
|
42
43
|
cancellation_token: Support::CancellationToken.new, workspace: nil, sandbox: nil)
|
|
44
|
+
entrypoint_class ||= assembly_class || agent_class
|
|
45
|
+
raise ArgumentError, "entrypoint_class is required" unless entrypoint_class
|
|
46
|
+
execution_class ||= assembly_class || entrypoint_class
|
|
47
|
+
|
|
43
48
|
@runtime = runtime
|
|
44
49
|
@agent_class = agent_class
|
|
45
50
|
@entrypoint_class = entrypoint_class
|
|
51
|
+
@execution_class = execution_class
|
|
46
52
|
@invocation = invocation
|
|
47
53
|
@cancellation_token = cancellation_token
|
|
48
54
|
@workspace = workspace
|
|
@@ -55,11 +61,15 @@ module LittleGhost
|
|
|
55
61
|
@event_mutex = Mutex.new
|
|
56
62
|
@subagent_instrumentation_mutex = Mutex.new
|
|
57
63
|
@subagent_instrumentation = {}
|
|
64
|
+
@assembly_step_instrumentation_mutex = Mutex.new
|
|
65
|
+
@assembly_step_instrumentation = {}
|
|
58
66
|
@exclusive_tools_mutex = Mutex.new
|
|
59
67
|
@once_mutex = Mutex.new
|
|
60
68
|
@once_keys = {}
|
|
61
69
|
@interruption_mutex = Mutex.new
|
|
70
|
+
@interruption_condition = ConditionVariable.new
|
|
62
71
|
@interruption_state = :not_started
|
|
72
|
+
@active_interruptions = 0
|
|
63
73
|
@entrypoint = nil
|
|
64
74
|
@usage = Usage.new
|
|
65
75
|
end
|
|
@@ -110,6 +120,21 @@ module LittleGhost
|
|
|
110
120
|
cancellation_token: Support::CancellationToken.new,
|
|
111
121
|
deadline: nil
|
|
112
122
|
)
|
|
123
|
+
interrupt_response_with do
|
|
124
|
+
[
|
|
125
|
+
message,
|
|
126
|
+
{
|
|
127
|
+
interruption_id:,
|
|
128
|
+
batch_key:,
|
|
129
|
+
metadata:,
|
|
130
|
+
cancellation_token:,
|
|
131
|
+
deadline:
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def interrupt_response_with # :nodoc:
|
|
113
138
|
entrypoint = @interruption_mutex.synchronize do
|
|
114
139
|
case @interruption_state
|
|
115
140
|
when :not_started, :starting
|
|
@@ -118,20 +143,22 @@ module LittleGhost
|
|
|
118
143
|
raise AgentInterruptError, "Run has already finished"
|
|
119
144
|
end
|
|
120
145
|
|
|
146
|
+
@active_interruptions += 1
|
|
121
147
|
@entrypoint
|
|
122
148
|
end
|
|
123
|
-
unless entrypoint.
|
|
149
|
+
unless entrypoint.respond_to?(:interrupt_response)
|
|
124
150
|
raise AgentInterruptError, "Run entrypoint does not support interruptions"
|
|
125
151
|
end
|
|
126
152
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
153
|
+
message, options = yield
|
|
154
|
+
entrypoint.interrupt_response(message, **options)
|
|
155
|
+
ensure
|
|
156
|
+
if entrypoint
|
|
157
|
+
@interruption_mutex.synchronize do
|
|
158
|
+
@active_interruptions -= 1
|
|
159
|
+
@interruption_condition.broadcast
|
|
160
|
+
end
|
|
161
|
+
end
|
|
135
162
|
end
|
|
136
163
|
|
|
137
164
|
# Creates a RunContext with this run's cancellation token and deadline.
|
|
@@ -209,6 +236,10 @@ module LittleGhost
|
|
|
209
236
|
outcome: cleanup_error ? :error : :cancelled,
|
|
210
237
|
error_type: cleanup_error&.class&.name
|
|
211
238
|
)
|
|
239
|
+
finish_remaining_assembly_step_instrumentation(
|
|
240
|
+
outcome: cleanup_error ? :error : :cancelled,
|
|
241
|
+
error_type: cleanup_error&.class&.name
|
|
242
|
+
)
|
|
212
243
|
rescue => error
|
|
213
244
|
errors << error
|
|
214
245
|
end
|
|
@@ -229,7 +260,7 @@ module LittleGhost
|
|
|
229
260
|
:run,
|
|
230
261
|
parent: nil,
|
|
231
262
|
**correlation_attributes,
|
|
232
|
-
entrypoint_kind
|
|
263
|
+
entrypoint_kind:,
|
|
233
264
|
workflow_name: workflow_run? ? entrypoint_name : nil,
|
|
234
265
|
trace_context: invocation[:parent_trace_context],
|
|
235
266
|
trace_links: invocation[:trace_links],
|
|
@@ -241,17 +272,13 @@ module LittleGhost
|
|
|
241
272
|
workspace&.open(run: self)
|
|
242
273
|
sandbox&.open(run: self)
|
|
243
274
|
@session = runtime.open_session(self)
|
|
244
|
-
agent =
|
|
245
|
-
runtime.build_agent(entrypoint_class, run: self)
|
|
246
|
-
else
|
|
247
|
-
entrypoint_class.new(run: self, runtime:)
|
|
248
|
-
end
|
|
275
|
+
agent = runtime.build_assembly(@execution_class, run: self)
|
|
249
276
|
@interruption_mutex.synchronize do
|
|
250
277
|
@entrypoint = agent
|
|
251
278
|
end
|
|
252
279
|
register(agent)
|
|
253
280
|
invoke = lambda do
|
|
254
|
-
history = session ?
|
|
281
|
+
history = session ? runtime.session_history(self, session, fallback: invocation.history) : invocation.history
|
|
255
282
|
context = session ? session.state.merge(invocation.context) : invocation.context.dup
|
|
256
283
|
options = {
|
|
257
284
|
history:,
|
|
@@ -333,9 +360,10 @@ module LittleGhost
|
|
|
333
360
|
]
|
|
334
361
|
ensure
|
|
335
362
|
@interruption_mutex.synchronize do
|
|
363
|
+
@interruption_state = :terminal
|
|
364
|
+
@interruption_condition.wait(@interruption_mutex) while @active_interruptions.positive?
|
|
336
365
|
@last_entrypoint = @entrypoint
|
|
337
366
|
@entrypoint = nil
|
|
338
|
-
@interruption_state = :terminal
|
|
339
367
|
end
|
|
340
368
|
resource_cleanup_error = nil
|
|
341
369
|
begin
|
|
@@ -402,25 +430,39 @@ module LittleGhost
|
|
|
402
430
|
end
|
|
403
431
|
|
|
404
432
|
def correlation_attributes
|
|
405
|
-
{
|
|
433
|
+
attributes = {
|
|
406
434
|
operation_id:,
|
|
407
435
|
run_id: invocation.run_id,
|
|
408
436
|
invocation_id: invocation.invocation_id,
|
|
409
437
|
session_id: invocation.session_id,
|
|
410
|
-
service_name: runtime.service_name
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
438
|
+
service_name: runtime.service_name
|
|
439
|
+
}
|
|
440
|
+
case entrypoint_kind
|
|
441
|
+
when :agent
|
|
442
|
+
attributes[:agent_id] = entrypoint_name
|
|
443
|
+
when :workflow
|
|
444
|
+
attributes[:workflow_name] = entrypoint_name
|
|
445
|
+
else
|
|
446
|
+
attributes[:assembly_id] = entrypoint_name
|
|
447
|
+
attributes[:assembly_kind] = entrypoint_kind
|
|
448
|
+
end
|
|
449
|
+
attributes
|
|
414
450
|
end
|
|
415
451
|
|
|
416
|
-
def workflow_run? = entrypoint_class <= Workflow
|
|
452
|
+
def workflow_run? = defined?(Workflow) && entrypoint_class <= Workflow
|
|
453
|
+
|
|
454
|
+
def entrypoint_kind
|
|
455
|
+
return entrypoint_class.assembly_kind if entrypoint_class.respond_to?(:assembly_kind)
|
|
456
|
+
|
|
457
|
+
workflow_run? ? :workflow : :agent
|
|
458
|
+
end
|
|
417
459
|
|
|
418
460
|
def entrypoint_name
|
|
419
461
|
return entrypoint_class.name.to_s if workflow_run?
|
|
420
462
|
|
|
421
|
-
return @entrypoint.entrypoint_name if @entrypoint
|
|
463
|
+
return @entrypoint.entrypoint_name if @entrypoint&.respond_to?(:entrypoint_name)
|
|
422
464
|
|
|
423
|
-
entrypoint_class.
|
|
465
|
+
entrypoint_class.respond_to?(:assembly_id) ? entrypoint_class.assembly_id : entrypoint_class.name.to_s
|
|
424
466
|
end
|
|
425
467
|
|
|
426
468
|
def instrument_subagent(data)
|
|
@@ -485,7 +527,53 @@ module LittleGhost
|
|
|
485
527
|
error_class ||= error if error.is_a?(String) && error.match?(/\A[A-Z]\w*(?:::[A-Z]\w*)*\z/)
|
|
486
528
|
attributes = data.slice(:attempt, :delay, :error_code, :http_status, :partial_text)
|
|
487
529
|
instrument(:model_retry, attributes.merge(error_class:).compact)
|
|
530
|
+
when :assembly_step_start
|
|
531
|
+
start_assembly_step_instrumentation(data)
|
|
532
|
+
when :assembly_step_stop
|
|
533
|
+
finish_assembly_step_instrumentation(data.fetch(:step_id), data.merge(outcome: :completed))
|
|
534
|
+
when :assembly_step_error
|
|
535
|
+
return unless data[:terminal]
|
|
536
|
+
|
|
537
|
+
finish_assembly_step_instrumentation(data.fetch(:step_id), data.merge(outcome: :error))
|
|
538
|
+
when :assembly_step_retry, :assembly_fork, :assembly_join, :assembly_transition, :assembly_handoff_loop
|
|
539
|
+
instrument(type, data.except(:usage))
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def start_assembly_step_instrumentation(attributes)
|
|
544
|
+
step_id = attributes.fetch(:step_id)
|
|
545
|
+
handle = Instrumentation.start(
|
|
546
|
+
:assembly_step,
|
|
547
|
+
parent: attributes[:parent_operation_id] || operation_id,
|
|
548
|
+
operation_id: step_id,
|
|
549
|
+
detached: true,
|
|
550
|
+
assembly_id: attributes[:assembly_id],
|
|
551
|
+
assembly_kind: attributes[:assembly_kind],
|
|
552
|
+
participant: attributes[:participant],
|
|
553
|
+
branch_id: attributes[:branch_id]
|
|
554
|
+
)
|
|
555
|
+
@assembly_step_instrumentation_mutex.synchronize { @assembly_step_instrumentation[step_id] = handle }
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def finish_assembly_step_instrumentation(step_id, attributes)
|
|
559
|
+
handle = @assembly_step_instrumentation_mutex.synchronize do
|
|
560
|
+
@assembly_step_instrumentation.delete(step_id)
|
|
561
|
+
end
|
|
562
|
+
return unless handle
|
|
563
|
+
|
|
564
|
+
usage = attributes[:usage]
|
|
565
|
+
handle.finish(
|
|
566
|
+
outcome: attributes[:outcome],
|
|
567
|
+
error_type: attributes[:error_type],
|
|
568
|
+
**(usage ? usage_attributes(usage) : {})
|
|
569
|
+
)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def finish_remaining_assembly_step_instrumentation(outcome:, error_type: nil)
|
|
573
|
+
handles = @assembly_step_instrumentation_mutex.synchronize do
|
|
574
|
+
@assembly_step_instrumentation.values.tap { @assembly_step_instrumentation.clear }
|
|
488
575
|
end
|
|
576
|
+
handles.reverse_each { |handle| handle.finish(outcome:, error_type:) }
|
|
489
577
|
end
|
|
490
578
|
|
|
491
579
|
def start_subagent_instrumentation(attributes)
|
|
@@ -4,16 +4,16 @@ module LittleGhost
|
|
|
4
4
|
# Associates a validated structured value with its declared schema name.
|
|
5
5
|
StructuredResult = Data.define(:schema_name, :value) # :nodoc:
|
|
6
6
|
|
|
7
|
-
# RunResult gives callers one final view of an
|
|
8
|
-
# It includes the response, usage, updated conversation, state,
|
|
9
|
-
# validated structured value.
|
|
7
|
+
# RunResult gives callers one final view of an assembly invocation.
|
|
8
|
+
# It includes the response, usage, updated conversation, state, coordination
|
|
9
|
+
# steps, and any validated structured value.
|
|
10
10
|
#
|
|
11
11
|
# Use #output when the caller should accept either structured or textual
|
|
12
12
|
# agents. It returns the validated structured value when present and #text
|
|
13
13
|
# otherwise.
|
|
14
|
-
RunResult = Data.define(:message, :stop_reason, :usage, :messages, :state, :structured_result) do # :nodoc:
|
|
15
|
-
def initialize(message:, stop_reason:, usage:, messages:, state:, structured_result: nil)
|
|
16
|
-
super
|
|
14
|
+
RunResult = Data.define(:message, :stop_reason, :usage, :messages, :state, :structured_result, :steps) do # :nodoc:
|
|
15
|
+
def initialize(message:, stop_reason:, usage:, messages:, state:, structured_result: nil, steps: [])
|
|
16
|
+
super(message:, stop_reason:, usage:, messages:, state:, structured_result:, steps: Array(steps).freeze)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# Reads the final message text, or an empty string when no message exists.
|
|
@@ -30,6 +30,9 @@ module LittleGhost
|
|
|
30
30
|
def output
|
|
31
31
|
structured? ? structured_result.value : text
|
|
32
32
|
end
|
|
33
|
+
|
|
34
|
+
# Returns immutable coordination queries for this result.
|
|
35
|
+
def trajectory = Assembly::Trajectory.new(steps)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
# Associates a validated structured value with its declared schema name.
|
|
@@ -56,9 +59,9 @@ module LittleGhost
|
|
|
56
59
|
# The locally validated application value.
|
|
57
60
|
end
|
|
58
61
|
|
|
59
|
-
# RunResult gives callers one final view of an
|
|
60
|
-
# It includes the response, usage, updated conversation, state,
|
|
61
|
-
# validated structured value.
|
|
62
|
+
# RunResult gives callers one final view of an Assembly invocation.
|
|
63
|
+
# It includes the response, usage, updated conversation, state, coordination
|
|
64
|
+
# steps, and any validated structured value.
|
|
62
65
|
#
|
|
63
66
|
# Use #output when the caller should accept either structured or textual
|
|
64
67
|
# agents. It returns the validated structured value when present and #text
|
|
@@ -68,9 +71,9 @@ module LittleGhost
|
|
|
68
71
|
# :singleton-method: new
|
|
69
72
|
# :call-seq:
|
|
70
73
|
# new(message:, stop_reason:, usage:, messages:, state:,
|
|
71
|
-
# structured_result: nil) -> RunResult
|
|
74
|
+
# structured_result: nil, steps: []) -> RunResult
|
|
72
75
|
#
|
|
73
|
-
# Creates the terminal value for one
|
|
76
|
+
# Creates the terminal value for one Assembly invocation.
|
|
74
77
|
|
|
75
78
|
##
|
|
76
79
|
# :attr_reader: message
|
|
@@ -96,6 +99,10 @@ module LittleGhost
|
|
|
96
99
|
# :attr_reader: structured_result
|
|
97
100
|
# The validated StructuredResult, or +nil+ for a textual result.
|
|
98
101
|
|
|
102
|
+
##
|
|
103
|
+
# :attr_reader: steps
|
|
104
|
+
# Immutable Assembly::Step records for composite invocations.
|
|
105
|
+
|
|
99
106
|
##
|
|
100
107
|
# :method: text
|
|
101
108
|
# Reads the final message text, or an empty string when no message exists.
|
|
@@ -107,5 +114,9 @@ module LittleGhost
|
|
|
107
114
|
##
|
|
108
115
|
# :method: output
|
|
109
116
|
# Uses the structured value when present and otherwise #text.
|
|
117
|
+
|
|
118
|
+
##
|
|
119
|
+
# :method: trajectory
|
|
120
|
+
# Returns an Assembly::Trajectory for querying immutable coordination steps.
|
|
110
121
|
end
|
|
111
122
|
end
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module LittleGhost
|
|
4
4
|
class Runtime
|
|
5
|
-
# Hooks let applications prepare runs,
|
|
6
|
-
# errors to caller-safe messages.
|
|
5
|
+
# Hooks let applications prepare runs, select session history, transform
|
|
6
|
+
# interruptions, and map errors to caller-safe messages.
|
|
7
7
|
#
|
|
8
8
|
# Hooks are instantiated once per Runtime in configuration order. Override
|
|
9
9
|
# only the methods needed and return the supplied value when leaving it
|
|
@@ -23,6 +23,11 @@ module LittleGhost
|
|
|
23
23
|
# Transforms an interruption payload before it reaches the agent.
|
|
24
24
|
def prepare_interruption(_run, payload) = payload
|
|
25
25
|
|
|
26
|
+
# Returns the history to use for this run, or nil to defer to later hooks
|
|
27
|
+
# and the session default. +stored+ is empty when the session is new;
|
|
28
|
+
# +fallback+ contains messages supplied by the invocation.
|
|
29
|
+
def session_history(_run, stored:, fallback:) = nil
|
|
30
|
+
|
|
26
31
|
# Returns a caller-safe error message, or nil to defer to later hooks and
|
|
27
32
|
# the runtime default. Avoid exposing secrets or internal exception text.
|
|
28
33
|
def error_message(_error, _run) = nil
|
data/lib/little_ghost/runtime.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
require_relative "configuration"
|
|
5
5
|
|
|
6
6
|
module LittleGhost
|
|
7
|
-
# Prepare the shared services that
|
|
7
|
+
# Prepare the shared services that assemblies use across many runs.
|
|
8
8
|
# A runtime owns model resolution, loading, persistence, lookup paths, hooks,
|
|
9
9
|
# and resource factories for one Ruby setup.
|
|
10
10
|
#
|
|
@@ -91,8 +91,8 @@ module LittleGhost
|
|
|
91
91
|
@prompt_paths = build_lookup_paths(:prompt_paths)
|
|
92
92
|
@skill_paths = build_lookup_paths(:skill_paths)
|
|
93
93
|
|
|
94
|
-
@startup_phase = "
|
|
95
|
-
@
|
|
94
|
+
@startup_phase = "agent_factory"
|
|
95
|
+
@agent_factory = AgentFactory.new(
|
|
96
96
|
runtime: self,
|
|
97
97
|
prompt_paths: @prompt_paths,
|
|
98
98
|
resolve_agent: method(:resolve_agent_class)
|
|
@@ -133,11 +133,19 @@ module LittleGhost
|
|
|
133
133
|
# sandbox resources to it.
|
|
134
134
|
def build_run(
|
|
135
135
|
payload,
|
|
136
|
-
agent_class
|
|
137
|
-
|
|
136
|
+
agent_class: nil,
|
|
137
|
+
assembly_class: nil,
|
|
138
|
+
entrypoint_class: nil,
|
|
139
|
+
execution_class: nil,
|
|
140
|
+
cancellation_token: Support::CancellationToken.new,
|
|
138
141
|
workspace: nil,
|
|
139
142
|
sandbox: nil
|
|
140
143
|
)
|
|
144
|
+
entrypoint_class ||= assembly_class || agent_class
|
|
145
|
+
raise ArgumentError, "entrypoint_class is required" unless entrypoint_class
|
|
146
|
+
|
|
147
|
+
execution_class ||= assembly_class || entrypoint_class
|
|
148
|
+
agent_class ||= entrypoint_class if entrypoint_class <= Agent
|
|
141
149
|
owned_resources = []
|
|
142
150
|
workspace ||= build_workspace.tap { |resource| owned_resources << resource }
|
|
143
151
|
sandbox ||= build_sandbox(workspace:).tap { |resource| owned_resources << resource }
|
|
@@ -146,6 +154,8 @@ module LittleGhost
|
|
|
146
154
|
runtime: self,
|
|
147
155
|
agent_class:,
|
|
148
156
|
entrypoint_class:,
|
|
157
|
+
execution_class:,
|
|
158
|
+
cancellation_token:,
|
|
149
159
|
workspace:,
|
|
150
160
|
sandbox:
|
|
151
161
|
)
|
|
@@ -183,7 +193,41 @@ module LittleGhost
|
|
|
183
193
|
tools: [],
|
|
184
194
|
agent_path: Subagents::AgentPath::ROOT
|
|
185
195
|
)
|
|
186
|
-
|
|
196
|
+
agent_class_or_name = agent_class_or_name.definition if agent_class_or_name.is_a?(AgentBuilder)
|
|
197
|
+
if agent_class_or_name.is_a?(AssemblyDefinition)
|
|
198
|
+
unless agent_class_or_name.kind == :agent
|
|
199
|
+
raise ConfigurationError, "agent definition must have kind :agent"
|
|
200
|
+
end
|
|
201
|
+
agent_class_or_name = agent_class_or_name.implementation
|
|
202
|
+
end
|
|
203
|
+
@agent_factory.build(agent_class_or_name, run:, model:, tools:, agent_path:)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Builds an Agent through AgentFactory or constructs another Assembly for +run+.
|
|
207
|
+
def build_assembly(assembly_class_or_name, run:, **options) # :nodoc:
|
|
208
|
+
if assembly_class_or_name.is_a?(AssemblyBuilder)
|
|
209
|
+
assembly_class_or_name = assembly_class_or_name.definition
|
|
210
|
+
end
|
|
211
|
+
if assembly_class_or_name.is_a?(Class) && assembly_class_or_name <= Assembly
|
|
212
|
+
assembly_class_or_name = assembly_class_or_name.definition
|
|
213
|
+
end
|
|
214
|
+
if assembly_class_or_name.is_a?(AssemblyDefinition)
|
|
215
|
+
return build_agent(assembly_class_or_name, run:, **options) if assembly_class_or_name.kind == :agent
|
|
216
|
+
raise ArgumentError, "composite assembly definitions do not accept agent build options" unless options.empty?
|
|
217
|
+
|
|
218
|
+
return assembly_class_or_name.implementation.new(run:, runtime: self)
|
|
219
|
+
end
|
|
220
|
+
if !assembly_class_or_name.is_a?(Class) || assembly_class_or_name <= Agent
|
|
221
|
+
return build_agent(assembly_class_or_name, run:, **options)
|
|
222
|
+
end
|
|
223
|
+
unless assembly_class_or_name <= Assembly
|
|
224
|
+
raise ConfigurationError, "assembly must inherit from LittleGhost::Assembly"
|
|
225
|
+
end
|
|
226
|
+
unless options.empty?
|
|
227
|
+
raise ArgumentError, "composite assemblies do not accept agent build options"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
assembly_class_or_name.new(run:, runtime: self)
|
|
187
231
|
end
|
|
188
232
|
|
|
189
233
|
# The low-cardinality service name attached to runtime telemetry.
|
|
@@ -205,6 +249,16 @@ module LittleGhost
|
|
|
205
249
|
)
|
|
206
250
|
end
|
|
207
251
|
|
|
252
|
+
def session_history(run, session, fallback:) # :nodoc:
|
|
253
|
+
stored = session.history
|
|
254
|
+
runtime_hooks.each do |hook|
|
|
255
|
+
history = hook.session_history(run, stored:, fallback:)
|
|
256
|
+
return normalize_history(history) unless history.nil?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
session.history(fallback:)
|
|
260
|
+
end
|
|
261
|
+
|
|
208
262
|
def prepare_run(run) # :nodoc:
|
|
209
263
|
runtime_hooks.each { |hook| hook.prepare_run(run) }
|
|
210
264
|
run
|
|
@@ -282,6 +336,10 @@ module LittleGhost
|
|
|
282
336
|
Array(hook_classes).map(&:new)
|
|
283
337
|
end
|
|
284
338
|
|
|
339
|
+
def normalize_history(history)
|
|
340
|
+
Array(history).map { |message| Message.coerce(message) }.freeze
|
|
341
|
+
end
|
|
342
|
+
|
|
285
343
|
def build_session_store(definition)
|
|
286
344
|
return SessionStores::Memory.new unless definition
|
|
287
345
|
|
data/lib/little_ghost/sandbox.rb
CHANGED
|
@@ -31,6 +31,8 @@ module LittleGhost
|
|
|
31
31
|
items.each_index { |index| queue << index }
|
|
32
32
|
results = Array.new(items.length)
|
|
33
33
|
errors = Array.new(items.length)
|
|
34
|
+
error_mutex = Mutex.new
|
|
35
|
+
first_worker_error = nil
|
|
34
36
|
worker_count = [@max_concurrency, items.length].min
|
|
35
37
|
execution_state = ExecutionState.capture
|
|
36
38
|
|
|
@@ -49,6 +51,8 @@ module LittleGhost
|
|
|
49
51
|
results[index] = block.call(items[index])
|
|
50
52
|
rescue => error
|
|
51
53
|
errors[index] = error
|
|
54
|
+
error_mutex.synchronize { first_worker_error ||= error }
|
|
55
|
+
cancellation_token.cancel
|
|
52
56
|
ensure
|
|
53
57
|
completions << index
|
|
54
58
|
end
|
|
@@ -56,16 +60,24 @@ module LittleGhost
|
|
|
56
60
|
end
|
|
57
61
|
end
|
|
58
62
|
end
|
|
63
|
+
callback_error = nil
|
|
59
64
|
begin
|
|
60
65
|
items.length.times do
|
|
61
66
|
index = completions.pop
|
|
62
|
-
|
|
67
|
+
begin
|
|
68
|
+
on_result.call(index, results[index]) if on_result && !errors[index]
|
|
69
|
+
rescue => error
|
|
70
|
+
callback_error = error
|
|
71
|
+
cancellation_token.cancel
|
|
72
|
+
break
|
|
73
|
+
end
|
|
63
74
|
end
|
|
64
75
|
ensure
|
|
65
76
|
workers.each(&:join)
|
|
66
77
|
end
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
raise callback_error if callback_error
|
|
80
|
+
first_error = errors.compact.find { |error| error.is_a?(CleanupError) } || first_worker_error
|
|
69
81
|
raise first_error if first_error
|
|
70
82
|
|
|
71
83
|
results
|
|
@@ -5,7 +5,7 @@ require "monitor"
|
|
|
5
5
|
|
|
6
6
|
module LittleGhost
|
|
7
7
|
module Support
|
|
8
|
-
# Loader finds agents and tools from a conventional application layout. Ruby
|
|
8
|
+
# Loader finds agents, assemblies, and tools from a conventional application layout. Ruby
|
|
9
9
|
# files map to constants by path, so applications can add extension classes
|
|
10
10
|
# without maintaining a manual require list.
|
|
11
11
|
#
|
|
@@ -16,7 +16,7 @@ module LittleGhost
|
|
|
16
16
|
# checked for symbolic-link escapes. Application load roots are trusted code,
|
|
17
17
|
# not a sandbox for untrusted files.
|
|
18
18
|
class Loader
|
|
19
|
-
DEFAULT_DIRECTORIES = %w[app/agents app/tools].freeze # :nodoc:
|
|
19
|
+
DEFAULT_DIRECTORIES = %w[app/agents app/assemblies app/tools].freeze # :nodoc:
|
|
20
20
|
PROCESS_LOCK = Monitor.new # :nodoc:
|
|
21
21
|
|
|
22
22
|
# Application root and configured relative load directories.
|
data/lib/little_ghost/support.rb
CHANGED
|
@@ -9,17 +9,29 @@ module LittleGhost
|
|
|
9
9
|
|
|
10
10
|
# Recursively duplicates hashes, arrays, and strings while preserving other
|
|
11
11
|
# values. Cyclic containers are not supported.
|
|
12
|
-
def deep_dup(value)
|
|
12
|
+
def deep_dup(value, ancestors = {})
|
|
13
|
+
if value.is_a?(Hash) || value.is_a?(Array) || value.is_a?(Data)
|
|
14
|
+
identity = value.object_id
|
|
15
|
+
raise ArgumentError, "cyclic values cannot be duplicated" if ancestors.key?(identity)
|
|
16
|
+
|
|
17
|
+
ancestors[identity] = true
|
|
18
|
+
end
|
|
13
19
|
case value
|
|
14
20
|
when Hash
|
|
15
|
-
value.each_with_object({})
|
|
21
|
+
value.each_with_object({}) do |(key, child), copy|
|
|
22
|
+
copy[deep_dup(key, ancestors)] = deep_dup(child, ancestors)
|
|
23
|
+
end
|
|
16
24
|
when Array
|
|
17
|
-
value.map { |child| deep_dup(child) }
|
|
25
|
+
value.map { |child| deep_dup(child, ancestors) }
|
|
18
26
|
when String
|
|
19
27
|
value.dup
|
|
28
|
+
when Data
|
|
29
|
+
value.class.new(**value.to_h.transform_values { |child| deep_dup(child, ancestors) })
|
|
20
30
|
else
|
|
21
31
|
value
|
|
22
32
|
end
|
|
33
|
+
ensure
|
|
34
|
+
ancestors.delete(identity) if identity
|
|
23
35
|
end
|
|
24
36
|
end
|
|
25
37
|
end
|