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
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module LittleGhost
|
|
6
|
+
class Assembly
|
|
7
|
+
MAX_STEP_OUTPUT_BYTES = 64 * 1024 # :nodoc:
|
|
8
|
+
MAX_STEP_EVENTS = 10_000 # :nodoc:
|
|
9
|
+
MAX_STEP_EVENT_BYTES = 10 * 1024 * 1024 # :nodoc:
|
|
10
|
+
|
|
11
|
+
Attempt = Data.define(:number, :status, :started_at, :finished_at, :usage, :error) do # :nodoc:
|
|
12
|
+
def initialize(number:, status:, started_at:, finished_at:, usage: Usage.new, error: nil)
|
|
13
|
+
super(number:, status: status.to_sym, started_at:, finished_at:, usage:, error: error&.to_s&.freeze)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Step = Data.define( # :nodoc:
|
|
18
|
+
:id, :parent_id, :predecessor_ids, :branch_id, :participant,
|
|
19
|
+
:assembly_id, :assembly_kind, :status, :attempts, :usage, :output, :output_truncated
|
|
20
|
+
) do
|
|
21
|
+
def initialize(id:, participant:, assembly_id:, assembly_kind:, status:, attempts:, usage:,
|
|
22
|
+
parent_id: nil, predecessor_ids: [], branch_id: nil, output: nil, output_truncated: false)
|
|
23
|
+
super(
|
|
24
|
+
id: id.to_s.freeze,
|
|
25
|
+
parent_id: parent_id&.to_s&.freeze,
|
|
26
|
+
predecessor_ids: Array(predecessor_ids).map { |value| value.to_s.freeze }.freeze,
|
|
27
|
+
branch_id: branch_id&.to_s&.freeze,
|
|
28
|
+
participant: participant.to_s.freeze,
|
|
29
|
+
assembly_id: assembly_id.to_s.freeze,
|
|
30
|
+
assembly_kind: assembly_kind.to_sym,
|
|
31
|
+
status: status.to_sym,
|
|
32
|
+
attempts: Array(attempts).freeze,
|
|
33
|
+
usage:,
|
|
34
|
+
output:,
|
|
35
|
+
output_truncated: output_truncated == true
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
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
|
+
|
|
125
|
+
# Immutable queries over the steps returned by one assembly invocation.
|
|
126
|
+
class Trajectory
|
|
127
|
+
include Enumerable
|
|
128
|
+
|
|
129
|
+
# Immutable steps in execution order.
|
|
130
|
+
attr_reader :steps
|
|
131
|
+
|
|
132
|
+
# Builds query indexes for +steps+.
|
|
133
|
+
def initialize(steps)
|
|
134
|
+
@steps = Array(steps).freeze
|
|
135
|
+
@by_id = @steps.to_h { |step| [step.id, step] }.freeze
|
|
136
|
+
freeze
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Iterates through steps in execution order.
|
|
140
|
+
def each(&block) = steps.each(&block)
|
|
141
|
+
# Finds one step by its stable ID.
|
|
142
|
+
def step(id) = @by_id[id.to_s]
|
|
143
|
+
# Returns steps whose parent is +id+.
|
|
144
|
+
def children(id) = steps.select { |item| item.parent_id == id.to_s }.freeze
|
|
145
|
+
# Returns attempts belonging to one step.
|
|
146
|
+
def attempts_for(id) = step(id)&.attempts || [].freeze
|
|
147
|
+
|
|
148
|
+
# Returns declared predecessor-to-step ID pairs.
|
|
149
|
+
def transitions
|
|
150
|
+
steps.flat_map do |item|
|
|
151
|
+
item.predecessor_ids.map { |predecessor| [predecessor, item.id].freeze }
|
|
152
|
+
end.freeze
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Indicates whether attempts from two steps overlapped in time.
|
|
156
|
+
def concurrent?(first_id, second_id)
|
|
157
|
+
first = step(first_id)
|
|
158
|
+
second = step(second_id)
|
|
159
|
+
return false unless first && second
|
|
160
|
+
|
|
161
|
+
first.attempts.any? do |left|
|
|
162
|
+
second.attempts.any? do |right|
|
|
163
|
+
left.started_at < right.finished_at && right.started_at < left.finished_at
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
StepExecution = Data.define(:result, :step, :events, :transition) # :nodoc:
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
def execute_assembly_step(
|
|
174
|
+
reference:,
|
|
175
|
+
participant:,
|
|
176
|
+
input:,
|
|
177
|
+
history:,
|
|
178
|
+
context:,
|
|
179
|
+
cancellation_token:,
|
|
180
|
+
deadline:,
|
|
181
|
+
settings:,
|
|
182
|
+
template_locals:,
|
|
183
|
+
template_paths:,
|
|
184
|
+
parent_operation_id:,
|
|
185
|
+
policies: {},
|
|
186
|
+
parent_id: nil,
|
|
187
|
+
predecessor_ids: [],
|
|
188
|
+
branch_id: nil,
|
|
189
|
+
checkpoint: nil,
|
|
190
|
+
build_options: {},
|
|
191
|
+
step_id: SecureRandom.uuid
|
|
192
|
+
)
|
|
193
|
+
policy = normalize_step_policy(policies)
|
|
194
|
+
attempts = []
|
|
195
|
+
usage = Usage.new
|
|
196
|
+
|
|
197
|
+
(policy.fetch(:retries) + 1).times do |index|
|
|
198
|
+
attempt_number = index + 1
|
|
199
|
+
started_at = Time.now
|
|
200
|
+
child = if build_options.any? && runtime.respond_to?(:build_agent)
|
|
201
|
+
runtime.build_agent(reference, run:, **build_options)
|
|
202
|
+
else
|
|
203
|
+
runtime.build_assembly(reference, run:, **build_options)
|
|
204
|
+
end
|
|
205
|
+
child_deadline = step_deadline(deadline, policy[:timeout])
|
|
206
|
+
attempt_events = []
|
|
207
|
+
attempt_event_bytes = 0
|
|
208
|
+
result = nil
|
|
209
|
+
attempt_usage = Usage.new
|
|
210
|
+
begin
|
|
211
|
+
with_active_assembly(child) do
|
|
212
|
+
child.stream(
|
|
213
|
+
input,
|
|
214
|
+
history:,
|
|
215
|
+
context: isolated_assembly_state(context),
|
|
216
|
+
cancellation_token:,
|
|
217
|
+
deadline: child_deadline,
|
|
218
|
+
settings:,
|
|
219
|
+
template_locals: template_locals.merge(runtime.template_locals(run:, agent: child)),
|
|
220
|
+
template_paths:,
|
|
221
|
+
parent_operation_id:,
|
|
222
|
+
checkpoint:
|
|
223
|
+
).each do |event|
|
|
224
|
+
result = event.data[:result] if event.type == :invocation_stop
|
|
225
|
+
attempt_usage = event.data[:usage] || attempt_usage if event.type == :invocation_error
|
|
226
|
+
attempt_event_bytes = buffer_assembly_event!(attempt_events, event, bytes: attempt_event_bytes)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
raise ProtocolError, "assembly step #{participant.inspect} did not return a result" unless result
|
|
230
|
+
|
|
231
|
+
attempt_usage = result.usage
|
|
232
|
+
usage += attempt_usage
|
|
233
|
+
attempts << Attempt.new(
|
|
234
|
+
number: attempt_number,
|
|
235
|
+
status: :completed,
|
|
236
|
+
started_at:,
|
|
237
|
+
finished_at: Time.now,
|
|
238
|
+
usage: attempt_usage
|
|
239
|
+
)
|
|
240
|
+
output, truncated = projected_step_output(result.output)
|
|
241
|
+
nested_steps = reparent_steps(result.steps, step_id)
|
|
242
|
+
step = Step.new(
|
|
243
|
+
id: step_id,
|
|
244
|
+
parent_id:,
|
|
245
|
+
predecessor_ids:,
|
|
246
|
+
branch_id:,
|
|
247
|
+
participant:,
|
|
248
|
+
assembly_id: child.class.respond_to?(:assembly_id) ? child.class.assembly_id : participant,
|
|
249
|
+
assembly_kind: child.class.respond_to?(:assembly_kind) ? child.class.assembly_kind : :assembly,
|
|
250
|
+
status: :completed,
|
|
251
|
+
attempts:,
|
|
252
|
+
usage:,
|
|
253
|
+
output:,
|
|
254
|
+
output_truncated: truncated
|
|
255
|
+
)
|
|
256
|
+
combined = copy_run_result(result, steps: [step, *nested_steps])
|
|
257
|
+
events = attempt_events.map do |event|
|
|
258
|
+
(event.type == :invocation_stop) ? StreamEvent.build(event.type, **event.data.merge(result: combined)) : event
|
|
259
|
+
end
|
|
260
|
+
transition = child.assembly_transition if child.respond_to?(:assembly_transition)
|
|
261
|
+
return StepExecution.new(result: combined, step:, events: events.freeze, transition:)
|
|
262
|
+
rescue => error
|
|
263
|
+
if own_step_timeout?(error, deadline, child_deadline, policy[:timeout])
|
|
264
|
+
error = AssemblyStepTimeoutError.new("assembly step #{participant.inspect} exceeded its timeout")
|
|
265
|
+
end
|
|
266
|
+
attempt_usage = error_usage(attempt_events, attempt_usage)
|
|
267
|
+
usage += attempt_usage
|
|
268
|
+
attempts << Attempt.new(
|
|
269
|
+
number: attempt_number,
|
|
270
|
+
status: :failed,
|
|
271
|
+
started_at:,
|
|
272
|
+
finished_at: Time.now,
|
|
273
|
+
usage: attempt_usage,
|
|
274
|
+
error: error.class.name
|
|
275
|
+
)
|
|
276
|
+
retrying = retry_step?(error, policy, index)
|
|
277
|
+
annotate_step_error(error, step_id:, attempts:, usage:, child:, participant:)
|
|
278
|
+
if block_given?
|
|
279
|
+
yield StreamEvent.build(
|
|
280
|
+
:assembly_step_error,
|
|
281
|
+
step_id:,
|
|
282
|
+
participant: participant.to_s,
|
|
283
|
+
attempt: attempt_number,
|
|
284
|
+
error_type: error.class.name,
|
|
285
|
+
usage:,
|
|
286
|
+
terminal: !retrying
|
|
287
|
+
)
|
|
288
|
+
end
|
|
289
|
+
raise error unless retrying
|
|
290
|
+
|
|
291
|
+
if block_given?
|
|
292
|
+
yield StreamEvent.build(
|
|
293
|
+
:assembly_step_retry,
|
|
294
|
+
step_id:,
|
|
295
|
+
participant: participant.to_s,
|
|
296
|
+
attempt: attempt_number,
|
|
297
|
+
error_type: error.class.name,
|
|
298
|
+
delay: policy.fetch(:retry_delay)
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
wait_for_retry(policy.fetch(:retry_delay), cancellation_token, deadline)
|
|
302
|
+
ensure
|
|
303
|
+
begin
|
|
304
|
+
child.close
|
|
305
|
+
rescue => cleanup_error
|
|
306
|
+
cleanup_usage = usage
|
|
307
|
+
annotate_step_error(
|
|
308
|
+
cleanup_error,
|
|
309
|
+
step_id:,
|
|
310
|
+
attempts:,
|
|
311
|
+
usage: cleanup_usage,
|
|
312
|
+
child:,
|
|
313
|
+
participant:
|
|
314
|
+
)
|
|
315
|
+
if block_given?
|
|
316
|
+
yield StreamEvent.build(
|
|
317
|
+
:assembly_step_error,
|
|
318
|
+
step_id:,
|
|
319
|
+
participant: participant.to_s,
|
|
320
|
+
attempt: attempt_number,
|
|
321
|
+
error_type: cleanup_error.class.name,
|
|
322
|
+
usage: cleanup_usage,
|
|
323
|
+
terminal: true
|
|
324
|
+
)
|
|
325
|
+
end
|
|
326
|
+
raise
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def normalize_step_policy(values)
|
|
333
|
+
self.class.validate_step_policy!(values)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
class << self
|
|
337
|
+
def validate_step_policy!(values) # :nodoc:
|
|
338
|
+
values = values.compact
|
|
339
|
+
retries = Integer(values.fetch(:retries, 0))
|
|
340
|
+
raise ArgumentError, "retries must be at least 0" if retries.negative?
|
|
341
|
+
|
|
342
|
+
retry_on = Array(values[:retry_on])
|
|
343
|
+
if retries.positive? && retry_on.empty?
|
|
344
|
+
raise ArgumentError, "retry_on is required when retries is greater than 0"
|
|
345
|
+
end
|
|
346
|
+
unless retry_on.all? { |error| error.is_a?(Class) && error <= Exception }
|
|
347
|
+
raise ArgumentError, "retry_on must contain exception classes"
|
|
348
|
+
end
|
|
349
|
+
timeout = Float(values[:timeout]) if values[:timeout]
|
|
350
|
+
retry_delay = Float(values.fetch(:retry_delay, 0))
|
|
351
|
+
raise ArgumentError, "timeout must be positive" if timeout && (!timeout.positive? || !timeout.finite?)
|
|
352
|
+
raise ArgumentError, "retry_delay must be non-negative" if retry_delay.negative? || !retry_delay.finite?
|
|
353
|
+
|
|
354
|
+
{retries:, retry_on: retry_on.freeze, timeout:, retry_delay:}.freeze
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def retry_step?(error, policy, index)
|
|
359
|
+
return false if index >= policy.fetch(:retries)
|
|
360
|
+
return false if error.is_a?(CancelledError) || error.is_a?(CleanupError)
|
|
361
|
+
|
|
362
|
+
policy.fetch(:retry_on).any? { |type| error.is_a?(type) }
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def step_deadline(parent_deadline, timeout)
|
|
366
|
+
local = Time.now + timeout if timeout
|
|
367
|
+
[parent_deadline, local].compact.min
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def own_step_timeout?(error, parent_deadline, child_deadline, timeout)
|
|
371
|
+
error.is_a?(DeadlineExceededError) && timeout && child_deadline && child_deadline != parent_deadline
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def wait_for_retry(delay, cancellation_token, deadline)
|
|
375
|
+
return if delay.zero?
|
|
376
|
+
|
|
377
|
+
stop_at = Time.now + delay
|
|
378
|
+
loop do
|
|
379
|
+
cancellation_token.raise_if_cancelled!
|
|
380
|
+
raise DeadlineExceededError, "The run deadline was reached" if deadline && Time.now >= deadline
|
|
381
|
+
|
|
382
|
+
remaining = stop_at - Time.now
|
|
383
|
+
break unless remaining.positive?
|
|
384
|
+
|
|
385
|
+
cancellation_token.wait([remaining, 0.05].min)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def error_usage(events, fallback)
|
|
390
|
+
events.reverse_each do |event|
|
|
391
|
+
return event.data[:usage] if event.type == :invocation_error && event.data[:usage]
|
|
392
|
+
end
|
|
393
|
+
fallback
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def annotate_step_error(error, step_id:, attempts:, usage:, child:, participant:)
|
|
397
|
+
error.instance_variable_set(:@little_ghost_step_id, step_id)
|
|
398
|
+
error.instance_variable_set(:@little_ghost_step_attempts, attempts.dup.freeze)
|
|
399
|
+
error.instance_variable_set(:@little_ghost_step_usage, usage)
|
|
400
|
+
error.instance_variable_set(
|
|
401
|
+
:@little_ghost_step_assembly_id,
|
|
402
|
+
child.class.respond_to?(:assembly_id) ? child.class.assembly_id : participant.to_s
|
|
403
|
+
)
|
|
404
|
+
error.instance_variable_set(
|
|
405
|
+
:@little_ghost_step_assembly_kind,
|
|
406
|
+
child.class.respond_to?(:assembly_kind) ? child.class.assembly_kind : :assembly
|
|
407
|
+
)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def projected_step_output(output)
|
|
411
|
+
text = output.is_a?(String) ? output : JSON.generate(output)
|
|
412
|
+
return [deep_frozen_assembly_value(output), false] if text.bytesize <= MAX_STEP_OUTPUT_BYTES
|
|
413
|
+
|
|
414
|
+
[nil, true]
|
|
415
|
+
rescue JSON::GeneratorError, TypeError
|
|
416
|
+
[nil, true]
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def reparent_steps(steps, parent_id)
|
|
420
|
+
Array(steps).map do |step|
|
|
421
|
+
next step if step.parent_id
|
|
422
|
+
|
|
423
|
+
Step.new(**step.to_h.merge(parent_id:))
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def copy_run_result(result, usage: result.usage, steps: result.steps)
|
|
428
|
+
RunResult.new(
|
|
429
|
+
message: result.message,
|
|
430
|
+
stop_reason: result.stop_reason,
|
|
431
|
+
usage:,
|
|
432
|
+
messages: result.messages,
|
|
433
|
+
state: result.state,
|
|
434
|
+
structured_result: result.structured_result,
|
|
435
|
+
steps:
|
|
436
|
+
)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def isolated_assembly_state(value)
|
|
440
|
+
case value
|
|
441
|
+
when Hash
|
|
442
|
+
value.to_h { |key, item| [isolated_assembly_state(key), isolated_assembly_state(item)] }
|
|
443
|
+
when Array
|
|
444
|
+
value.map { |item| isolated_assembly_state(item) }
|
|
445
|
+
when String
|
|
446
|
+
value.dup
|
|
447
|
+
when NilClass, TrueClass, FalseClass, Numeric, Symbol
|
|
448
|
+
value
|
|
449
|
+
else
|
|
450
|
+
raise ArgumentError, "assembly context must contain only JSON-like state"
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def deep_frozen_assembly_value(value)
|
|
455
|
+
copied = isolated_assembly_state(value)
|
|
456
|
+
case copied
|
|
457
|
+
when Hash
|
|
458
|
+
copied.each do |key, item|
|
|
459
|
+
deep_freeze_assembly_value(key)
|
|
460
|
+
deep_freeze_assembly_value(item)
|
|
461
|
+
end
|
|
462
|
+
when Array
|
|
463
|
+
copied.each { |item| deep_freeze_assembly_value(item) }
|
|
464
|
+
end
|
|
465
|
+
copied.freeze
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def deep_freeze_assembly_value(value)
|
|
469
|
+
case value
|
|
470
|
+
when Hash
|
|
471
|
+
value.each do |key, item|
|
|
472
|
+
deep_freeze_assembly_value(key)
|
|
473
|
+
deep_freeze_assembly_value(item)
|
|
474
|
+
end
|
|
475
|
+
when Array
|
|
476
|
+
value.each { |item| deep_freeze_assembly_value(item) }
|
|
477
|
+
end
|
|
478
|
+
value.freeze
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def buffer_assembly_event!(buffer, event, bytes:)
|
|
482
|
+
raise AssemblyLimitError, "assembly step emitted too many events" if buffer.length >= MAX_STEP_EVENTS
|
|
483
|
+
|
|
484
|
+
bytes += buffered_assembly_size(event)
|
|
485
|
+
raise AssemblyLimitError, "assembly step emitted too much event data" if bytes > MAX_STEP_EVENT_BYTES
|
|
486
|
+
|
|
487
|
+
buffer << event
|
|
488
|
+
bytes
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def buffered_assembly_size(value, ancestors = {}, depth = 0)
|
|
492
|
+
return MAX_STEP_EVENT_BYTES + 1 if depth > 32
|
|
493
|
+
|
|
494
|
+
identity = value.object_id
|
|
495
|
+
return 0 if ancestors.key?(identity)
|
|
496
|
+
|
|
497
|
+
case value
|
|
498
|
+
when String
|
|
499
|
+
value.bytesize
|
|
500
|
+
when Hash
|
|
501
|
+
ancestors[identity] = true
|
|
502
|
+
value.sum do |key, item|
|
|
503
|
+
buffered_assembly_size(key, ancestors, depth + 1) + buffered_assembly_size(item, ancestors, depth + 1)
|
|
504
|
+
end
|
|
505
|
+
when Array
|
|
506
|
+
ancestors[identity] = true
|
|
507
|
+
value.sum { |item| buffered_assembly_size(item, ancestors, depth + 1) }
|
|
508
|
+
when Data
|
|
509
|
+
ancestors[identity] = true
|
|
510
|
+
value.members.sum { |member| buffered_assembly_size(value.public_send(member), ancestors, depth + 1) }
|
|
511
|
+
else
|
|
512
|
+
64
|
|
513
|
+
end
|
|
514
|
+
ensure
|
|
515
|
+
ancestors.delete(identity) if identity
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def enqueue_assembly_event(queue, value, cancellation_token)
|
|
519
|
+
loop do
|
|
520
|
+
cancellation_token.raise_if_cancelled!
|
|
521
|
+
queue.push(value, true)
|
|
522
|
+
return
|
|
523
|
+
rescue ThreadError
|
|
524
|
+
cancellation_token.wait(0.01)
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def enqueue_assembly_terminal(queue, value)
|
|
529
|
+
queue.push(value, true)
|
|
530
|
+
rescue ThreadError
|
|
531
|
+
queue.pop(true)
|
|
532
|
+
retry
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
end
|