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 and standalone ask method return the Run after work finishes. For a live interface, use the class-level streaming helper or standalone streaming method. The stream yields StreamEvent objects, and enumeration returns the same Run with its final outcome and response. 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

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.

The Run opens its workspace, sandbox, Session, and Assembly entrypoint, then closes registered resources in reverse order. register adds application resources to that lifecycle. Interjection is available only while one Agent entrypoint is active.

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)


79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/little_ghost/run.rb', line 79

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 = {}
  @interjection_mutex = Mutex.new
  @interjection_condition = ConditionVariable.new
  @interjection_state = :not_started
  @active_interjections = 0
  @entrypoint = nil
  @usage = Usage.new
end

Instance Attribute Details

#agent_classObject (readonly)

Agent class used for compatibility when the entrypoint is an Agent.



52
53
54
# File 'lib/little_ghost/run.rb', line 52

def agent_class
  @agent_class
end

#cancellation_tokenObject (readonly)

Token that cooperatively stops this Run and its children.



58
59
60
# File 'lib/little_ghost/run.rb', line 58

def cancellation_token
  @cancellation_token
end

#entrypoint_classObject (readonly)

Public Agent, Workflow, Swarm, or Graph class selected by the caller.



54
55
56
# File 'lib/little_ghost/run.rb', line 54

def entrypoint_class
  @entrypoint_class
end

#errorObject (readonly)

Exception that caused a failed, partial, or cancelled outcome.



68
69
70
# File 'lib/little_ghost/run.rb', line 68

def error
  @error
end

#invocationObject (readonly)

Normalized request carried by this Run.



56
57
58
# File 'lib/little_ghost/run.rb', line 56

def invocation
  @invocation
end

#operation_idObject (readonly)

Unique identifier for this top-level operation.



62
63
64
# File 'lib/little_ghost/run.rb', line 62

def operation_id
  @operation_id
end

#outcomeObject (readonly)

Terminal String: completed, failed, partial, or cancelled.



64
65
66
# File 'lib/little_ghost/run.rb', line 64

def outcome
  @outcome
end

#responseObject (readonly)

Caller-facing final text, or the partial text preserved at a deadline.



66
67
68
# File 'lib/little_ghost/run.rb', line 66

def response
  @response
end

#resultObject (readonly)

Final RunResult, when the Assembly produced one.



60
61
62
# File 'lib/little_ghost/run.rb', line 60

def result
  @result
end

#runtimeObject (readonly)

Runtime that built and executes this Run.



50
51
52
# File 'lib/little_ghost/run.rb', line 50

def runtime
  @runtime
end

#sandboxObject (readonly)

Request-scoped sandbox owned or supplied by the Run.



76
77
78
# File 'lib/little_ghost/run.rb', line 76

def sandbox
  @sandbox
end

#sessionObject (readonly)

Session opened for this invocation, when persistence is configured.



70
71
72
# File 'lib/little_ghost/run.rb', line 70

def session
  @session
end

#usageObject (readonly)

Normalized Usage accumulated by the Run.



72
73
74
# File 'lib/little_ghost/run.rb', line 72

def usage
  @usage
end

#workspaceObject (readonly)

Request-scoped workspace owned or supplied by the Run.



74
75
76
# File 'lib/little_ghost/run.rb', line 74

def workspace
  @workspace
end

Instance Method Details

#callObject

Consumes the event stream and returns self.



116
117
118
119
# File 'lib/little_ghost/run.rb', line 116

def call
  each { |_event| }
  self
end

#cancelled?Boolean

True when cancellation stopped the run without a response.

Returns:

  • (Boolean)


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

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.



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
# File 'lib/little_ghost/run.rb', line 259

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)


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

def completed? = outcome == "completed"

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

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



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

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.



124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/little_ghost/run.rb', line 124

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)


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

def failed? = outcome == "failed"

#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.



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/little_ghost/run.rb', line 153

def interject(
  message,
  interjection_id: nil,
  batch_key: nil,
  metadata: {},
  cancellation_token: Support::CancellationToken.new,
  deadline: nil
)
  interject_with do
    [
      message,
      {
        interjection_id:,
        batch_key:,
        metadata:,
        cancellation_token:,
        deadline:
      }
    ]
  end
end

#interject_withObject

:nodoc:



175
176
177
178
179
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 175

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

  message, options = yield
  entrypoint.interject(message, **options)
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.



241
242
243
244
245
246
247
248
249
# File 'lib/little_ghost/run.rb', line 241

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)


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

def partial? = outcome == "partial"

#prepare_interjection(payload) ⇒ Object

:nodoc:



251
252
253
# File 'lib/little_ghost/run.rb', line 251

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

#publish(type, **data) ⇒ Object

:nodoc:



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

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.



223
224
225
226
227
228
229
230
# File 'lib/little_ghost/run.rb', line 223

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:



232
233
234
# File 'lib/little_ghost/run.rb', line 232

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