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.
- checksums.yaml +4 -4
- data/README.md +72 -74
- data/docs/guides/assemblies.md +286 -0
- data/docs/guides/core_concepts.md +159 -135
- data/docs/guides/getting_started.md +114 -83
- 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 +35 -8
- data/lib/little_ghost/agent/tool_loop.rb +2 -1
- data/lib/little_ghost/agent.rb +280 -326
- data/lib/little_ghost/agent_builder.rb +20 -4
- data/lib/little_ghost/agent_factory.rb +3 -0
- data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
- data/lib/little_ghost/assembly.rb +345 -0
- data/lib/little_ghost/assembly_builder.rb +497 -0
- data/lib/little_ghost/assembly_execution.rb +535 -0
- 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 +10 -2
- data/lib/little_ghost/execution.rb +206 -0
- data/lib/little_ghost/graph.rb +930 -0
- 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 +190 -64
- data/lib/little_ghost/run_context.rb +33 -20
- data/lib/little_ghost/run_result.rb +22 -11
- data/lib/little_ghost/runtime/hook.rb +9 -4
- data/lib/little_ghost/runtime.rb +134 -36
- data/lib/little_ghost/sandbox.rb +1 -1
- 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/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 +439 -0
- data/lib/little_ghost/tool.rb +88 -20
- data/lib/little_ghost/tools/write_todos.rb +6 -1
- data/lib/little_ghost/tracing/open_telemetry.rb +14 -3
- data/lib/little_ghost/unrestricted_sandbox.rb +1 -1
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost/workflow.rb +224 -90
- data/lib/little_ghost.rb +36 -25
- 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
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
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
|
|
9
|
-
#
|
|
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 =
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
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
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# JSON-like context is copied for each
|
|
43
|
-
# from mutating a sibling's state.
|
|
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
|
|
47
|
-
# already consumed invocation, or consuming
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
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:,
|
|
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
|
-
@
|
|
63
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
131
|
-
attr_reader :run
|
|
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
|
|
134
|
-
|
|
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
|
|
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 +
|
|
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
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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/
|
|
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
|
-
#
|
|
68
|
-
# sit inside an existing Ruby system or
|
|
69
|
-
#
|
|
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
|
-
#
|
|
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
|
|
83
|
-
#
|
|
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
|
|
89
|
+
# run.response
|
|
90
|
+
# # One possible response: Transfer 481 is waiting for the receiving bank.
|
|
90
91
|
#
|
|
91
|
-
# Agent subclasses hold reusable behavior
|
|
92
|
-
# and Run objects own execution and cleanup.
|
|
93
|
-
#
|
|
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.
|
|
97
|
-
#
|
|
98
|
-
#
|
|
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+
|
|
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.
|
|
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
|
|
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/
|
|
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
|
|
257
|
+
summary: A Ruby framework for AI features with agents and composable assemblies
|
|
246
258
|
test_files: []
|