Class: LittleGhost::Run
- Inherits:
-
Object
- Object
- LittleGhost::Run
- Includes:
- Enumerable
- Defined in:
- lib/little_ghost/run.rb
Overview
Observe one top-level assembly execution from start to finish. A run records its response, outcome, usage, error, and owned resources.
run = CustomerSupportAgent.ask("Why is transfer 481 pending?")
run.completed? # => true
run.outcome # => "completed"
run.response # => "Transfer 481 is waiting for the receiving bank."
ask returns the Run after work finishes. stream_ask yields StreamEvent
objects as work happens, then returns the same finished Run. A Run executes
only once.
stream = CustomerSupportAgent.stream_ask("Where is transfer 481?")
run = stream.each do |event|
publish(event) if event.type == :text_delta
end
run.completed? # => true
run.response
Outcomes
Completion, failure, deadline, and cancellation become the completed,
failed, partial, and cancelled outcomes. Ordinary execution failures
are available through error and the terminal stream event. Failures while
closing resources, delivering events, or reporting instrumentation may
still raise because LittleGhost cannot report a reliable ending.
Tool validation and ToolError failures return safe Tool results to the model,
which may recover and complete the Run. Input, configuration, or resource
construction can raise before a Run exists. Once execution begins, terminal
events are run_stop, run_error, run_partial, and run_cancel.
Owned resources
The Run opens its workspace, sandbox, Session, and Assembly entrypoint, then
closes registered resources in reverse order. register adds application
resources to that cleanup sequence. Interjection is available only while one
Agent entrypoint is active.
Nested Agent events
A composite Assembly stream observes every Agent that shares the Run. Each
:agent_stream event carries an AgentStreamSource in data[:source] and a
copied, frozen Agent StreamEvent in data[:event]. An inner
:invocation_start also includes the copied, frozen Message sent to that
Agent in data[:input]. Event consumers cannot change the running work.
Parallel Agents may interleave, but the Run invokes the stream consumer
serially. Contextual events expose data from every participating Agent, so
applications should enable include_agent_events only for destinations
that may see every participant's data.
Instance Attribute Summary collapse
-
#agent_class ⇒ Object
readonly
Agent class used for compatibility when the entrypoint is an Agent.
-
#cancellation_token ⇒ Object
readonly
Token that cooperatively stops this Run and its children.
-
#entrypoint_class ⇒ Object
readonly
Public Agent, Workflow, Swarm, or Graph class selected by the caller.
-
#error ⇒ Object
readonly
Exception that caused a failed, partial, or cancelled outcome.
-
#invocation ⇒ Object
readonly
Normalized request carried by this Run.
-
#operation_id ⇒ Object
readonly
Unique identifier for this top-level operation.
-
#outcome ⇒ Object
readonly
Terminal String:
completed,failed,partial, orcancelled. -
#response ⇒ Object
readonly
Caller-facing final text, or the partial text preserved at a deadline.
-
#result ⇒ Object
readonly
Final RunResult, when the Assembly produced one.
-
#runtime ⇒ Object
readonly
Runtime that built and executes this Run.
-
#sandbox ⇒ Object
readonly
Request-scoped sandbox owned or supplied by the Run.
-
#session ⇒ Object
readonly
Session opened for this invocation, when persistence is configured.
-
#usage ⇒ Object
readonly
Normalized Usage accumulated by the Run.
-
#workspace ⇒ Object
readonly
Request-scoped workspace owned or supplied by the Run.
Instance Method Summary collapse
-
#call ⇒ Object
Consumes the event stream and returns
self. -
#cancelled? ⇒ Boolean
True when cancellation stopped the run without a response.
-
#close ⇒ Object
Closes registered resources in reverse order.
-
#completed? ⇒ Boolean
True after successful completion.
-
#context(state: {}, metadata: {}) ⇒ Object
Creates a RunContext with this run's cancellation token and deadline.
-
#each ⇒ Object
Yields events and returns
selfafter the terminal event. -
#failed? ⇒ Boolean
True after execution or cleanup failed.
-
#include_agent_events? ⇒ Boolean
Indicates whether the stream includes contextual events from every Agent that executes as part of this Run.
-
#initialize(invocation:, runtime:, agent_class: nil, assembly_class: nil, entrypoint_class: nil, execution_class: nil, cancellation_token: Support::CancellationToken.new, workspace: nil, sandbox: nil, include_agent_events_by_default: false) ⇒ Run
constructor
Creates a dormant run for
invocation. -
#interject(message, interjection_id: nil, batch_key: nil, metadata: {}, cancellation_token: Support::CancellationToken.new, deadline: nil) ⇒ Object
Adds an interjection to the active entrypoint and waits for its response.
-
#interject_with ⇒ Object
:nodoc:.
-
#once(key) ⇒ Object
Performs the block at most once successfully for
keyduring this run. -
#partial? ⇒ Boolean
True when the deadline preserved a partial response.
-
#prepare_interjection(payload) ⇒ Object
:nodoc:.
-
#publish(type, **data) ⇒ Object
:nodoc:.
-
#register(resource = nil, &closer) ⇒ Object
Adds a resource or closer to reverse-order cleanup and returns the resource.
-
#shared_resource(key) ⇒ Object
:nodoc:.
-
#synchronize_exclusive_tools(&block) ⇒ Object
:nodoc:.
Constructor Details
#initialize(invocation:, runtime:, agent_class: nil, assembly_class: nil, entrypoint_class: nil, execution_class: nil, cancellation_token: Support::CancellationToken.new, workspace: nil, sandbox: nil, include_agent_events_by_default: false) ⇒ Run
Creates a dormant run for invocation.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/little_ghost/run.rb', line 92 def initialize(invocation:, runtime:, agent_class: nil, assembly_class: nil, entrypoint_class: nil, execution_class: nil, cancellation_token: Support::CancellationToken.new, workspace: nil, sandbox: nil, include_agent_events_by_default: false) entrypoint_class ||= assembly_class || agent_class raise ArgumentError, "entrypoint_class is required" unless entrypoint_class execution_class ||= assembly_class || entrypoint_class @runtime = runtime @agent_class = agent_class @entrypoint_class = entrypoint_class @execution_class = execution_class @invocation = invocation @cancellation_token = cancellation_token @workspace = workspace @sandbox = sandbox @operation_id = SecureRandom.uuid @resources = [] @shared_resources = {} @shared_resource_condition = ConditionVariable.new @closed = false @started = false @mutex = Mutex.new @event_mutex = Mutex.new @subagent_instrumentation_mutex = Mutex.new @subagent_instrumentation = {} @assembly_step_instrumentation_mutex = Mutex.new @assembly_step_instrumentation = {} @exclusive_tools_mutex = Mutex.new @once_mutex = Mutex.new @once_keys = {} @interjection_mutex = Mutex.new @interjection_condition = ConditionVariable.new @interjection_state = :not_started @active_interjections = 0 @entrypoint = nil @usage = Usage.new include_agent_events = invocation[:include_agent_events] unless include_agent_events.nil? || include_agent_events == true || include_agent_events == false raise InvocationError, "include_agent_events must be true or false" end @include_agent_events = include_agent_events.nil? ? include_agent_events_by_default : include_agent_events end |
Instance Attribute Details
#agent_class ⇒ Object (readonly)
Agent class used for compatibility when the entrypoint is an Agent.
65 66 67 |
# File 'lib/little_ghost/run.rb', line 65 def agent_class @agent_class end |
#cancellation_token ⇒ Object (readonly)
Token that cooperatively stops this Run and its children.
71 72 73 |
# File 'lib/little_ghost/run.rb', line 71 def cancellation_token @cancellation_token end |
#entrypoint_class ⇒ Object (readonly)
Public Agent, Workflow, Swarm, or Graph class selected by the caller.
67 68 69 |
# File 'lib/little_ghost/run.rb', line 67 def entrypoint_class @entrypoint_class end |
#error ⇒ Object (readonly)
Exception that caused a failed, partial, or cancelled outcome.
81 82 83 |
# File 'lib/little_ghost/run.rb', line 81 def error @error end |
#invocation ⇒ Object (readonly)
Normalized request carried by this Run.
69 70 71 |
# File 'lib/little_ghost/run.rb', line 69 def invocation @invocation end |
#operation_id ⇒ Object (readonly)
Unique identifier for this top-level operation.
75 76 77 |
# File 'lib/little_ghost/run.rb', line 75 def operation_id @operation_id end |
#outcome ⇒ Object (readonly)
Terminal String: completed, failed, partial, or cancelled.
77 78 79 |
# File 'lib/little_ghost/run.rb', line 77 def outcome @outcome end |
#response ⇒ Object (readonly)
Caller-facing final text, or the partial text preserved at a deadline.
79 80 81 |
# File 'lib/little_ghost/run.rb', line 79 def response @response end |
#result ⇒ Object (readonly)
Final RunResult, when the Assembly produced one.
73 74 75 |
# File 'lib/little_ghost/run.rb', line 73 def result @result end |
#runtime ⇒ Object (readonly)
Runtime that built and executes this Run.
63 64 65 |
# File 'lib/little_ghost/run.rb', line 63 def runtime @runtime end |
#sandbox ⇒ Object (readonly)
Request-scoped sandbox owned or supplied by the Run.
89 90 91 |
# File 'lib/little_ghost/run.rb', line 89 def sandbox @sandbox end |
#session ⇒ Object (readonly)
Session opened for this invocation, when persistence is configured.
83 84 85 |
# File 'lib/little_ghost/run.rb', line 83 def session @session end |
#usage ⇒ Object (readonly)
Normalized Usage accumulated by the Run.
85 86 87 |
# File 'lib/little_ghost/run.rb', line 85 def usage @usage end |
#workspace ⇒ Object (readonly)
Request-scoped workspace owned or supplied by the Run.
87 88 89 |
# File 'lib/little_ghost/run.rb', line 87 def workspace @workspace end |
Instance Method Details
#call ⇒ Object
Consumes the event stream and returns self.
137 138 139 140 |
# File 'lib/little_ghost/run.rb', line 137 def call each { |_event| } self end |
#cancelled? ⇒ Boolean
True when cancellation stopped the run without a response.
170 |
# File 'lib/little_ghost/run.rb', line 170 def cancelled? = outcome == "cancelled" |
#close ⇒ Object
Closes registered resources in reverse order.
The operation is idempotent. It attempts every closer and then raises the first LittleGhost::CleanupError, or otherwise the first cleanup exception.
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 |
# File 'lib/little_ghost/run.rb', line 326 def close callbacks = @mutex.synchronize do return if @closed @closed = true @shared_resources.clear @shared_resource_condition.broadcast @resources.reverse end errors = [] callbacks.each do |callback| callback.call rescue => error errors << error end cleanup_error = errors.find { |caught| caught.is_a?(CleanupError) } || errors.first begin finish_remaining_subagent_instrumentation( outcome: cleanup_error ? :error : :cancelled, error_type: cleanup_error&.class&.name ) finish_remaining_assembly_step_instrumentation( outcome: cleanup_error ? :error : :cancelled, error_type: cleanup_error&.class&.name ) rescue => error errors << error end error = errors.find { |caught| caught.is_a?(CleanupError) } || errors.first raise error if error end |
#completed? ⇒ Boolean
True after successful completion.
161 |
# File 'lib/little_ghost/run.rb', line 161 def completed? = outcome == "completed" |
#context(state: {}, metadata: {}) ⇒ Object
Creates a RunContext with this run's cancellation token and deadline.
230 231 232 233 234 235 236 237 |
# File 'lib/little_ghost/run.rb', line 230 def context(state: {}, metadata: {}) RunContext.new( state:, cancellation_token:, deadline: invocation.deadline_at, metadata: ) end |
#each ⇒ Object
Yields events and returns self after the terminal event.
Without a block, returns an Enumerator. A second execution raises Error.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/little_ghost/run.rb', line 145 def each return enum_for(__method__) unless block_given? begin_execution! @emitter = lambda do |event| @event_mutex.synchronize { yield_event(event) { |value| yield value } } end Instrumentation.with_context(correlation_attributes.except(:operation_id)) do execute { |event| @emitter.call(event) } end self ensure @emitter = nil end |
#failed? ⇒ Boolean
True after execution or cleanup failed.
164 |
# File 'lib/little_ghost/run.rb', line 164 def failed? = outcome == "failed" |
#include_agent_events? ⇒ Boolean
Indicates whether the stream includes contextual events from every Agent that executes as part of this Run.
174 |
# File 'lib/little_ghost/run.rb', line 174 def include_agent_events? = @include_agent_events |
#interject(message, interjection_id: nil, batch_key: nil, metadata: {}, cancellation_token: Support::CancellationToken.new, deadline: nil) ⇒ Object
Adds an interjection to the active entrypoint and waits for its response.
Raises LittleGhost::AgentInterjectionError before the entrypoint is ready, after it finishes, or when the entrypoint does not support interjections.
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/little_ghost/run.rb', line 180 def interject( , interjection_id: nil, batch_key: nil, metadata: {}, cancellation_token: Support::CancellationToken.new, deadline: nil ) interject_with do [ , { interjection_id:, batch_key:, metadata:, cancellation_token:, deadline: } ] end end |
#interject_with ⇒ Object
:nodoc:
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/little_ghost/run.rb', line 202 def interject_with # :nodoc: entrypoint = @interjection_mutex.synchronize do case @interjection_state when :not_started, :starting raise AgentInterjectionError, "Run entrypoint is not ready for interjections" when :terminal raise AgentInterjectionError, "Run has already finished" end @active_interjections += 1 @entrypoint end unless entrypoint.respond_to?(:interject) raise AgentInterjectionError, "Run entrypoint does not support interjections" end , = yield entrypoint.interject(, **) ensure if entrypoint @interjection_mutex.synchronize do @active_interjections -= 1 @interjection_condition.broadcast end end end |
#once(key) ⇒ Object
Performs the block at most once successfully for key during this run.
Concurrent callers are serialized. The caller that performs the block
receives its value; later callers receive nil. If the block raises, the
key is not recorded and a later call may retry it.
308 309 310 311 312 313 314 315 316 |
# File 'lib/little_ghost/run.rb', line 308 def once(key) @once_mutex.synchronize do return if @once_keys.key?(key) value = yield @once_keys[key] = true value end end |
#partial? ⇒ Boolean
True when the deadline preserved a partial response.
167 |
# File 'lib/little_ghost/run.rb', line 167 def partial? = outcome == "partial" |
#prepare_interjection(payload) ⇒ Object
:nodoc:
318 319 320 |
# File 'lib/little_ghost/run.rb', line 318 def prepare_interjection(payload) # :nodoc: runtime.prepare_interjection(self, payload) end |
#publish(type, **data) ⇒ Object
:nodoc:
239 240 241 242 243 244 |
# File 'lib/little_ghost/run.rb', line 239 def publish(type, **data) # :nodoc: event = StreamEvent.build(type, **data) @emitter&.call(event) instrument_event(type, data) event end |
#register(resource = nil, &closer) ⇒ Object
Adds a resource or closer to reverse-order cleanup and returns the resource.
A resource must respond to close unless a block supplies the cleanup
operation. Registering after the run has closed raises Error.
250 251 252 253 254 255 256 257 |
# File 'lib/little_ghost/run.rb', line 250 def register(resource = nil, &closer) callback = closer || close_callback(resource) @mutex.synchronize do raise Error, "run is already closed" if @closed @resources << callback end resource end |
#shared_resource(key) ⇒ Object
:nodoc:
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/little_ghost/run.rb', line 259 def shared_resource(key) # :nodoc: raise ArgumentError, "shared_resource requires a block" unless block_given? entry = @mutex.synchronize do loop do raise Error, "run is already closed" if @closed existing = @shared_resources[key] if existing return existing.fetch(:value) if existing[:ready] @shared_resource_condition.wait(@mutex) next end created = {ready: false, value: nil} @shared_resources[key] = created break created end end value = yield @mutex.synchronize do unless @shared_resources[key].equal?(entry) && !@closed raise Error, "run closed while a shared resource was starting" end entry[:value] = value entry[:ready] = true @shared_resource_condition.broadcast end value rescue @mutex.synchronize do @shared_resources.delete(key) if @shared_resources[key].equal?(entry) @shared_resource_condition.broadcast end raise end |
#synchronize_exclusive_tools(&block) ⇒ Object
:nodoc:
299 300 301 |
# File 'lib/little_ghost/run.rb', line 299 def synchronize_exclusive_tools(&block) # :nodoc: @exclusive_tools_mutex.synchronize(&block) end |