Class: LittleGhost::Run

Inherits:
Object
  • Object
show all
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."

The class-level ask helper or standalone ask method consumes the event stream and returns the Run. For a live interface, the class-level streaming helper or standalone streaming method yields StreamEvent objects and returns the same run after enumeration. A run can execute only once.

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; cleanup, event delivery, or instrumentation failures may still raise because the framework cannot safely report a clean stop.

The run opens its workspace, sandbox, session, and assembly entrypoint, then closes registered resources in reverse order. register extends that lifecycle for application resources. Interruption is available only while an agent entrypoint is active and unambiguous.

Instance Attribute Summary collapse

Instance Method Summary collapse

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) ⇒ Run

Creates a dormant run for invocation.

Raises:

  • (ArgumentError)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/little_ghost/run.rb', line 41

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)
  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 = []
  @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 = {}
  @interruption_mutex = Mutex.new
  @interruption_condition = ConditionVariable.new
  @interruption_state = :not_started
  @active_interruptions = 0
  @entrypoint = nil
  @usage = Usage.new
end

Instance Attribute Details

#agent_classObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def agent_class
  @agent_class
end

#cancellation_tokenObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def cancellation_token
  @cancellation_token
end

#entrypoint_classObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def entrypoint_class
  @entrypoint_class
end

#errorObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def error
  @error
end

#invocationObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def invocation
  @invocation
end

#operation_idObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def operation_id
  @operation_id
end

#outcomeObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def outcome
  @outcome
end

#responseObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def response
  @response
end

#resultObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def result
  @result
end

#runtimeObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def runtime
  @runtime
end

#sandboxObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def sandbox
  @sandbox
end

#sessionObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def session
  @session
end

#usageObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def usage
  @usage
end

#workspaceObject (readonly)

Runtime and declarations used to execute the run; its request, cancellation token, resources, terminal outcome, response, result, usage, and error.



37
38
39
# File 'lib/little_ghost/run.rb', line 37

def workspace
  @workspace
end

Instance Method Details

#callObject

Consumes the event stream and returns self.



78
79
80
81
# File 'lib/little_ghost/run.rb', line 78

def call
  each { |_event| }
  self
end

#cancelled?Boolean

True when cancellation stopped the run without a response.

Returns:

  • (Boolean)


109
# File 'lib/little_ghost/run.rb', line 109

def cancelled? = outcome == "cancelled"

#closeObject

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.



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/little_ghost/run.rb', line 221

def close
  callbacks = @mutex.synchronize do
    return if @closed
    @closed = true
    @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.

Returns:

  • (Boolean)


100
# File 'lib/little_ghost/run.rb', line 100

def completed? = outcome == "completed"

#context(state: {}, metadata: {}) ⇒ Object

Creates a RunContext with this run's cancellation token and deadline.



165
166
167
168
169
170
171
172
# File 'lib/little_ghost/run.rb', line 165

def context(state: {}, metadata: {})
  RunContext.new(
    state:,
    cancellation_token:,
    deadline: invocation.deadline_at,
    metadata:
  )
end

#eachObject

Yields events and returns self after the terminal event.

Without a block, returns an Enumerator. A second execution raises Error.



86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/little_ghost/run.rb', line 86

def each
  return enum_for(__method__) unless block_given?

  begin_execution!
  @emitter = ->(event) { yield_event(event) { |value| yield value } }
  Instrumentation.with_context(correlation_attributes.except(:operation_id)) do
    execute { |event| yield event }
  end
  self
ensure
  @emitter = nil
end

#failed?Boolean

True after execution or cleanup failed.

Returns:

  • (Boolean)


103
# File 'lib/little_ghost/run.rb', line 103

def failed? = outcome == "failed"

#interrupt_response(message, interruption_id: nil, batch_key: nil, metadata: {}, cancellation_token: Support::CancellationToken.new, deadline: nil) ⇒ Object

Adds an interruption to the active entrypoint and waits for its response.

Raises LittleGhost::AgentInterruptError before the entrypoint is ready, after it finishes, or when the entrypoint does not support interruptions.



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/little_ghost/run.rb', line 115

def interrupt_response(
  message,
  interruption_id: nil,
  batch_key: nil,
  metadata: {},
  cancellation_token: Support::CancellationToken.new,
  deadline: nil
)
  interrupt_response_with do
    [
      message,
      {
        interruption_id:,
        batch_key:,
        metadata:,
        cancellation_token:,
        deadline:
      }
    ]
  end
end

#interrupt_response_withObject

:nodoc:



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/little_ghost/run.rb', line 137

def interrupt_response_with # :nodoc:
  entrypoint = @interruption_mutex.synchronize do
    case @interruption_state
    when :not_started, :starting
      raise AgentInterruptError, "Run entrypoint is not ready for interruptions"
    when :terminal
      raise AgentInterruptError, "Run has already finished"
    end

    @active_interruptions += 1
    @entrypoint
  end
  unless entrypoint.respond_to?(:interrupt_response)
    raise AgentInterruptError, "Run entrypoint does not support interruptions"
  end

  message, options = yield
  entrypoint.interrupt_response(message, **options)
ensure
  if entrypoint
    @interruption_mutex.synchronize do
      @active_interruptions -= 1
      @interruption_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.



203
204
205
206
207
208
209
210
211
# File 'lib/little_ghost/run.rb', line 203

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.

Returns:

  • (Boolean)


106
# File 'lib/little_ghost/run.rb', line 106

def partial? = outcome == "partial"

#prepare_interruption(payload) ⇒ Object

:nodoc:



213
214
215
# File 'lib/little_ghost/run.rb', line 213

def prepare_interruption(payload) # :nodoc:
  runtime.prepare_interruption(self, payload)
end

#publish(type, **data) ⇒ Object

:nodoc:



174
175
176
177
178
179
# File 'lib/little_ghost/run.rb', line 174

def publish(type, **data) # :nodoc:
  event = StreamEvent.build(type, **data)
  @event_mutex.synchronize { @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.



185
186
187
188
189
190
191
192
# File 'lib/little_ghost/run.rb', line 185

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

#synchronize_exclusive_tools(&block) ⇒ Object

:nodoc:



194
195
196
# File 'lib/little_ghost/run.rb', line 194

def synchronize_exclusive_tools(&block) # :nodoc:
  @exclusive_tools_mutex.synchronize(&block)
end