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,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LittleGhost
|
|
4
|
+
# DataMap holds JSON-compatible application data with indifferent key access.
|
|
5
|
+
# It stores every key as a String while accepting String and Symbol keys for
|
|
6
|
+
# lookup and mutation, including in nested maps.
|
|
7
|
+
#
|
|
8
|
+
# state = DataMap.new(plan: {status: "active"})
|
|
9
|
+
# state.dig("plan", :status) # => "active"
|
|
10
|
+
# state.to_h # => {"plan" => {"status" => "active"}}
|
|
11
|
+
#
|
|
12
|
+
# State and metadata exposed by Sessions and RunContexts use DataMap so they
|
|
13
|
+
# remain easy to work with in Ruby and portable across session stores. Values
|
|
14
|
+
# are limited to JSON primitives, Arrays, and mappings. A mapping that
|
|
15
|
+
# supplies both a String and Symbol form of the same key is ambiguous and
|
|
16
|
+
# raises ArgumentError.
|
|
17
|
+
class DataMap < Hash
|
|
18
|
+
alias_method :store_raw_value, :[]=
|
|
19
|
+
private :store_raw_value
|
|
20
|
+
|
|
21
|
+
# Builds a deeply normalized map from +value+.
|
|
22
|
+
def initialize(value = {})
|
|
23
|
+
super()
|
|
24
|
+
replace(value)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns +value+ when it is already a DataMap, or normalizes a mapping.
|
|
28
|
+
def self.coerce(value)
|
|
29
|
+
return value if value.is_a?(self)
|
|
30
|
+
|
|
31
|
+
new(value)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Looks up +key+ after canonicalizing it to a String.
|
|
35
|
+
def [](key) = super(normalize_key(key))
|
|
36
|
+
|
|
37
|
+
# Stores +value+ under the canonical String form of +key+.
|
|
38
|
+
def []=(key, value)
|
|
39
|
+
super(normalize_key(key), normalize_value(value))
|
|
40
|
+
end
|
|
41
|
+
alias_method :store, :[]=
|
|
42
|
+
|
|
43
|
+
# Fetches +key+ with Hash#fetch's default and block behavior.
|
|
44
|
+
def fetch(key, *defaults, &block) = super(normalize_key(key), *defaults, &block)
|
|
45
|
+
|
|
46
|
+
# Checks for +key+ after canonicalizing it to a String.
|
|
47
|
+
def key?(key) = super(normalize_key(key))
|
|
48
|
+
alias_method :has_key?, :key?
|
|
49
|
+
alias_method :include?, :key?
|
|
50
|
+
alias_method :member?, :key?
|
|
51
|
+
|
|
52
|
+
# Removes +key+ after canonicalizing it to a String.
|
|
53
|
+
def delete(key, &block) = super(normalize_key(key), &block)
|
|
54
|
+
|
|
55
|
+
# Traverses nested DataMaps with String or Symbol keys.
|
|
56
|
+
def dig(key, *names) = super(normalize_key(key), *names)
|
|
57
|
+
|
|
58
|
+
# Returns a normalized copy merged with +other+.
|
|
59
|
+
def merge(other, &block)
|
|
60
|
+
dup.merge!(other, &block)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Merges +other+ after deeply normalizing its keys and values.
|
|
64
|
+
def merge!(other)
|
|
65
|
+
canonical_pairs(other).each do |key, value|
|
|
66
|
+
self[key] = (block_given? && key?(key)) ? yield(key, self[key], value) : value
|
|
67
|
+
end
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
alias_method :update, :merge!
|
|
71
|
+
|
|
72
|
+
# Replaces all entries with a deeply normalized copy of +other+.
|
|
73
|
+
def replace(other)
|
|
74
|
+
pairs = canonical_pairs(other)
|
|
75
|
+
clear
|
|
76
|
+
pairs.each { |key, value| self[key] = value }
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns a deep independent DataMap copy.
|
|
81
|
+
def initialize_copy(other)
|
|
82
|
+
super
|
|
83
|
+
replace(other.to_h)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Produces a deep ordinary Hash with canonical String keys.
|
|
87
|
+
def to_h
|
|
88
|
+
plain_value(self)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def canonical_pairs(value, normalize_values: true)
|
|
94
|
+
hash = Hash.try_convert(value)
|
|
95
|
+
raise ArgumentError, "DataMap requires a mapping" unless hash
|
|
96
|
+
|
|
97
|
+
seen = {}
|
|
98
|
+
hash.map do |key, child|
|
|
99
|
+
normalized = normalize_key(key)
|
|
100
|
+
raise ArgumentError, "DataMap keys must not contain both String and Symbol forms" if seen[normalized]
|
|
101
|
+
|
|
102
|
+
seen[normalized] = true
|
|
103
|
+
[normalized, normalize_values ? normalize_value(child) : child]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def normalize_key(key)
|
|
108
|
+
return key if key.is_a?(String)
|
|
109
|
+
return key.to_s if key.is_a?(Symbol)
|
|
110
|
+
|
|
111
|
+
raise ArgumentError, "DataMap keys must be Strings or Symbols"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def normalize_value(value)
|
|
115
|
+
return normalize_scalar(value) unless container?(value)
|
|
116
|
+
|
|
117
|
+
copy = container_copy(value)
|
|
118
|
+
stack = [[:visit, value, copy]]
|
|
119
|
+
ancestors = {}
|
|
120
|
+
until stack.empty?
|
|
121
|
+
action, current, target = stack.pop
|
|
122
|
+
if action == :leave
|
|
123
|
+
ancestors.delete(current)
|
|
124
|
+
next
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
raise ArgumentError, "DataMap cannot contain cyclic values" if ancestors[current.object_id]
|
|
128
|
+
|
|
129
|
+
ancestors[current.object_id] = true
|
|
130
|
+
stack << [:leave, current.object_id, nil]
|
|
131
|
+
if current.is_a?(Hash)
|
|
132
|
+
canonical_pairs(current, normalize_values: false).reverse_each do |key, child|
|
|
133
|
+
child_copy = container?(child) ? container_copy(child) : normalize_scalar(child)
|
|
134
|
+
target.send(:store_raw_value, key, child_copy)
|
|
135
|
+
stack << [:visit, child, child_copy] if container?(child)
|
|
136
|
+
end
|
|
137
|
+
else
|
|
138
|
+
current.each_with_index do |child, index|
|
|
139
|
+
child_copy = container?(child) ? container_copy(child) : normalize_scalar(child)
|
|
140
|
+
target[index] = child_copy
|
|
141
|
+
stack << [:visit, child, child_copy] if container?(child)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
copy
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def plain_value(value)
|
|
150
|
+
return value unless container?(value)
|
|
151
|
+
|
|
152
|
+
copy = value.is_a?(Array) ? [] : {}
|
|
153
|
+
stack = [[:visit, value, copy]]
|
|
154
|
+
ancestors = {}
|
|
155
|
+
until stack.empty?
|
|
156
|
+
action, current, target = stack.pop
|
|
157
|
+
if action == :leave
|
|
158
|
+
ancestors.delete(current)
|
|
159
|
+
next
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
raise ArgumentError, "DataMap cannot contain cyclic values" if ancestors[current.object_id]
|
|
163
|
+
|
|
164
|
+
ancestors[current.object_id] = true
|
|
165
|
+
stack << [:leave, current.object_id, nil]
|
|
166
|
+
if current.is_a?(Hash)
|
|
167
|
+
canonical_pairs(current, normalize_values: false).each do |key, child|
|
|
168
|
+
child_copy = container?(child) ? plain_container_copy(child) : normalize_scalar(child)
|
|
169
|
+
target[key] = child_copy
|
|
170
|
+
stack << [:visit, child, child_copy] if container?(child)
|
|
171
|
+
end
|
|
172
|
+
else
|
|
173
|
+
current.each_with_index do |child, index|
|
|
174
|
+
child_copy = container?(child) ? plain_container_copy(child) : normalize_scalar(child)
|
|
175
|
+
target[index] = child_copy
|
|
176
|
+
stack << [:visit, child, child_copy] if container?(child)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
copy
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def container?(value)
|
|
185
|
+
value.is_a?(Hash) || value.is_a?(Array)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def container_copy(value)
|
|
189
|
+
value.is_a?(Array) ? [] : self.class.new
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def plain_container_copy(value)
|
|
193
|
+
value.is_a?(Array) ? [] : {}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def normalize_scalar(value)
|
|
197
|
+
case value
|
|
198
|
+
when String, Integer, TrueClass, FalseClass, NilClass
|
|
199
|
+
value
|
|
200
|
+
when Float
|
|
201
|
+
raise ArgumentError, "DataMap values must be JSON-compatible" unless value.finite?
|
|
202
|
+
|
|
203
|
+
value
|
|
204
|
+
else
|
|
205
|
+
raise ArgumentError, "DataMap values must be JSON-compatible"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
data/lib/little_ghost/errors.rb
CHANGED
|
@@ -13,6 +13,14 @@ module LittleGhost
|
|
|
13
13
|
class CredentialError < ConfigurationError; end
|
|
14
14
|
# Raised when an invocation payload or operation is invalid.
|
|
15
15
|
class InvocationError < Error; end
|
|
16
|
+
# Base class for failures coordinating one or more assemblies.
|
|
17
|
+
class AssemblyError < Error; end
|
|
18
|
+
# Raised when an assembly exceeds its configured execution bound.
|
|
19
|
+
class AssemblyLimitError < AssemblyError; end
|
|
20
|
+
# Raised when an assembly cannot choose one valid next participant.
|
|
21
|
+
class AssemblyRoutingError < AssemblyError; end
|
|
22
|
+
# Raised when one assembly step exceeds its local timeout.
|
|
23
|
+
class AssemblyStepTimeoutError < AssemblyError; end
|
|
16
24
|
# Raised when an invocation contains an unsupported input form.
|
|
17
25
|
class UnsupportedInputError < InvocationError; end
|
|
18
26
|
# Base class for provider response and protocol failures.
|
|
@@ -43,8 +51,8 @@ module LittleGhost
|
|
|
43
51
|
class ToolLoopError < ProtocolError; end
|
|
44
52
|
# Base class for failures while executing a tool.
|
|
45
53
|
class ToolError < Error; end
|
|
46
|
-
# Raised when an active run cannot accept an
|
|
47
|
-
class
|
|
54
|
+
# Raised when an active run cannot accept an interjection.
|
|
55
|
+
class AgentInterjectionError < InvocationError; end
|
|
48
56
|
# Raised when cancellation stops an operation.
|
|
49
57
|
class CancelledError < Error; end
|
|
50
58
|
# Raised when an operation reaches its deadline.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LittleGhost
|
|
4
|
+
# Runs one dormant Run in a supervised worker while the caller remains free to
|
|
5
|
+
# serve health checks, deliver interjections, or coordinate process shutdown.
|
|
6
|
+
#
|
|
7
|
+
# execution = agent.start_execution(message: "Investigate transfer 481") do |event|
|
|
8
|
+
# event_buffer << event
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# execution.interject(message: "Include the latest ledger entry")
|
|
12
|
+
# execution.wait(deadline: Time.now + 30)
|
|
13
|
+
# execution.run.completed? # => true
|
|
14
|
+
#
|
|
15
|
+
# An execution owns its worker. The worker receives a snapshot of the caller's
|
|
16
|
+
# request-scoped ExecutionState. The Run continues to own its workspace,
|
|
17
|
+
# sandbox, session, entrypoint, and registered resources. +close+ requests
|
|
18
|
+
# cooperative cancellation and waits for both the worker and in-flight
|
|
19
|
+
# interjection calls.
|
|
20
|
+
class Execution
|
|
21
|
+
# The supervised Run and an exception raised outside the Run's ordinary
|
|
22
|
+
# terminal outcome, such as event delivery or cleanup failure.
|
|
23
|
+
attr_reader :run
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# Starts +run+ immediately and returns its supervising Execution.
|
|
27
|
+
#
|
|
28
|
+
# The optional block receives each StreamEvent on the worker thread. It
|
|
29
|
+
# must be safe to call from that thread and should not retain sensitive
|
|
30
|
+
# event content longer than the application requires.
|
|
31
|
+
def start(run, &event_consumer)
|
|
32
|
+
new(run, event_consumer:).send(:start)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(run, event_consumer: nil) # :nodoc:
|
|
37
|
+
raise ArgumentError, "run must be a LittleGhost::Run" unless run.is_a?(Run)
|
|
38
|
+
unless event_consumer.nil? || event_consumer.respond_to?(:call)
|
|
39
|
+
raise ArgumentError, "event consumer must be callable"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@run = run
|
|
43
|
+
@event_consumer = event_consumer
|
|
44
|
+
@state = :pending
|
|
45
|
+
@error = nil
|
|
46
|
+
@mutex = Mutex.new
|
|
47
|
+
@condition = ConditionVariable.new
|
|
48
|
+
@active_interjections = 0
|
|
49
|
+
@closing = false
|
|
50
|
+
@execution_state = ExecutionState.capture
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns +:pending+, +:running+, or +:finished+.
|
|
54
|
+
def state
|
|
55
|
+
@mutex.synchronize { @state }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns an event-delivery or cleanup exception raised by the worker.
|
|
59
|
+
def error
|
|
60
|
+
@mutex.synchronize { @error }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Indicates that the worker or an interjection call is still active.
|
|
64
|
+
def active?
|
|
65
|
+
@mutex.synchronize { @state != :finished || @active_interjections.positive? }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Indicates that the worker and all interjection calls have finished.
|
|
69
|
+
def finished?
|
|
70
|
+
!active?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Prepares and delivers one interjection to the active run.
|
|
74
|
+
#
|
|
75
|
+
# +payload+ may be a message or a Hash containing +message+ and the options
|
|
76
|
+
# accepted by Run#interject. Runtime hooks receive the Hash before
|
|
77
|
+
# delivery, allowing them to materialize trusted application attachments.
|
|
78
|
+
# Calls may overlap, but +close+ prevents new calls and waits for calls that
|
|
79
|
+
# have already begun.
|
|
80
|
+
def interject(payload = nil, **options)
|
|
81
|
+
if payload.nil? && options.key?(:message)
|
|
82
|
+
payload = options.delete(:message)
|
|
83
|
+
end
|
|
84
|
+
interjection_started = false
|
|
85
|
+
begin_interjection!
|
|
86
|
+
interjection_started = true
|
|
87
|
+
run.interject_with do
|
|
88
|
+
prepared = run.prepare_interjection(interjection_payload(payload, options))
|
|
89
|
+
interjection_arguments(prepared, options)
|
|
90
|
+
end
|
|
91
|
+
ensure
|
|
92
|
+
finish_interjection! if interjection_started
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Requests cooperative cancellation and returns +self+.
|
|
96
|
+
def cancel
|
|
97
|
+
run.cancellation_token.cancel
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Waits for the worker and in-flight interjections, then returns the Run.
|
|
102
|
+
#
|
|
103
|
+
# +deadline+ is an absolute Time. Reaching it raises DeadlineExceededError
|
|
104
|
+
# without cancelling the run. An event-delivery or cleanup failure raised by
|
|
105
|
+
# the worker is re-raised after all supervised work finishes.
|
|
106
|
+
def wait(deadline: nil)
|
|
107
|
+
worker = wait_until_finished(deadline:)
|
|
108
|
+
worker.join
|
|
109
|
+
caught = error
|
|
110
|
+
raise caught if caught
|
|
111
|
+
|
|
112
|
+
run
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Prevents new interjections, requests cancellation, and waits for shutdown.
|
|
116
|
+
# The operation is idempotent. +deadline+ has the same meaning as in #wait.
|
|
117
|
+
def close(deadline: nil)
|
|
118
|
+
@mutex.synchronize { @closing = true }
|
|
119
|
+
cancel
|
|
120
|
+
wait(deadline:)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
protected
|
|
124
|
+
|
|
125
|
+
def start # :nodoc:
|
|
126
|
+
@mutex.synchronize do
|
|
127
|
+
raise Error, "execution has already started" unless @state == :pending
|
|
128
|
+
|
|
129
|
+
@state = :running
|
|
130
|
+
@worker = Thread.new { execute }
|
|
131
|
+
end
|
|
132
|
+
self
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def execute
|
|
138
|
+
ExecutionState.with(@execution_state) do
|
|
139
|
+
run.each { |event| @event_consumer&.call(event) }
|
|
140
|
+
end
|
|
141
|
+
rescue => caught
|
|
142
|
+
@mutex.synchronize { @error = caught }
|
|
143
|
+
ensure
|
|
144
|
+
@mutex.synchronize do
|
|
145
|
+
@state = :finished
|
|
146
|
+
@condition.broadcast
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def begin_interjection!
|
|
151
|
+
@mutex.synchronize do
|
|
152
|
+
raise AgentInterjectionError, "Execution is closing" if @closing
|
|
153
|
+
raise AgentInterjectionError, "Execution has already finished" if @state == :finished
|
|
154
|
+
|
|
155
|
+
@active_interjections += 1
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def finish_interjection!
|
|
160
|
+
@mutex.synchronize do
|
|
161
|
+
@active_interjections -= 1 if @active_interjections.positive?
|
|
162
|
+
@condition.broadcast
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def interjection_payload(payload, options)
|
|
167
|
+
values = payload.is_a?(Hash) ? payload.dup : {message: payload}
|
|
168
|
+
options.each { |key, value| values[key] = value }
|
|
169
|
+
values
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def interjection_arguments(prepared, fallback)
|
|
173
|
+
unless prepared.is_a?(Hash)
|
|
174
|
+
return [prepared, fallback.slice(:interjection_id, :batch_key, :metadata, :cancellation_token, :deadline)]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
values = prepared.transform_keys(&:to_sym)
|
|
178
|
+
message = values.delete(:message) { raise ArgumentError, "prepared interjection must include a message" }
|
|
179
|
+
allowed = values.slice(:interjection_id, :batch_key, :metadata, :cancellation_token, :deadline)
|
|
180
|
+
[message, fallback.slice(:interjection_id, :batch_key, :metadata, :cancellation_token, :deadline).merge(allowed)]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def wait_until_finished(deadline:)
|
|
184
|
+
monotonic_deadline = if deadline
|
|
185
|
+
monotonic_time + [deadline - Time.now, 0].max
|
|
186
|
+
end
|
|
187
|
+
@mutex.synchronize do
|
|
188
|
+
until @state == :finished && @active_interjections.zero?
|
|
189
|
+
if monotonic_deadline
|
|
190
|
+
remaining = monotonic_deadline - monotonic_time
|
|
191
|
+
raise DeadlineExceededError, "Execution did not finish before the wait deadline" unless remaining.positive?
|
|
192
|
+
|
|
193
|
+
@condition.wait(@mutex, remaining)
|
|
194
|
+
else
|
|
195
|
+
@condition.wait(@mutex)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
@worker
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def monotonic_time
|
|
203
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|