Class: LittleGhost::Runtime

Inherits:
Object
  • Object
show all
Defined in:
lib/little_ghost/runtime.rb,
lib/little_ghost/runtime/hook.rb,
lib/little_ghost/runtime/hooks/artifacts.rb

Overview

Owns the shared services that assemblies reuse across many Runs.

Most applications do not construct this class. Configure LittleGhost once and call a named Agent or Assembly; the first standalone call lazily builds LittleGhost.runtime, and later calls reuse it automatically. Each call still receives a fresh Run, bound participants, Tools, workspace, and sandbox.

Construct Runtime directly when one process intentionally hosts an isolated LittleGhost setup:

configuration = LittleGhost::Configuration.new(
root: Dir.pwd,
providers: {
  openrouter: {adapter: :openrouter, api_key: ENV.fetch("OPENROUTER_API_KEY")}
},
models: {customer_support: {target: "openrouter:openai/gpt-5.6-luna"}},
default_model: :customer_support,
service_name: "support-api"
)
runtime = LittleGhost::Runtime.new(configuration: configuration)

CustomerSupportAgent.new(runtime: runtime)
.ask("Where is order 481?")
.response

Explicit construction snapshots the supplied Configuration but does not replace LittleGhost's shared default Runtime.

A Runtime may build independent Runs concurrently. Each Run gets fresh participants and Tools. By default, it also gets a Runtime-created Workspace and Sandbox that the Run owns. Instances supplied by the application remain caller-owned.

Advanced construction and ownership

Normal construction reads the application's configured definitions and builds shared model resolution, persistence, hooks, and resource factories. The settings form and #build are lower-level extension points for deriving another Runtime from an existing configuration snapshot.

#build_run creates a workspace and sandbox when needed. Once the Run owns them, it closes them; if construction stops halfway through, Runtime closes the partial resources. Startup failures are reported to instrumentation and then raised. Session actor resolution must use authenticated application identity. The default Sandboxes::Unrestricted uses host permissions and is not a security boundary for untrusted work.

Shared stores, resolvers, hooks, subscribers, providers, and resource factories may receive concurrent calls. Calls can overlap on different threads, or fibers can take turns entering the same object on one thread. Extensions must protect shared mutable state without relying on thread identity. One SessionStore instance serializes calls for the same Session. A store must provide its own coordination across processes.

See Running in Production for choosing a concurrency backend and protecting shared extensions.

Runtime has no shutdown operation. Runs close resources created for their request. The application shuts down shared services and process-wide Instrumentation subscribers with the rest of the process.

Defined Under Namespace

Modules: Hooks Classes: Hook

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(configuration:, settings: nil) ⇒ Runtime

Starts a runtime from configuration or an existing settings snapshot.



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
135
136
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
163
164
165
166
167
168
169
# File 'lib/little_ghost/runtime.rb', line 98

def initialize(configuration:, settings: nil)
  @startup_started_at = monotonic_time
  @startup_phase = "configuration"
  @startup_reported = false

  begin
    raise ArgumentError, "configuration must be a LittleGhost::Configuration" unless configuration.is_a?(Configuration)

    @configuration = configuration
    if settings
      @settings = settings
    else
      bootstrap_root = canonical_application_root(configuration.root)
      configuration.load_file!(root: bootstrap_root)
      @settings = configuration.settings(root: bootstrap_root)
    end
    @task_runner = Support::TaskRunner.new(
      backend: @settings.fetch(:concurrency_backend, :auto)
    )
    report_startup(status: "starting")
    @startup_reported = true
    @root = canonical_application_root(@settings.fetch(:root))
    @skill_resource_root = @settings[:skill_resource_root]
    @workspace_declaration = @settings.fetch(:workspace)
    @sandbox_declaration = @settings.fetch(:sandbox)
    @code_mode_configuration = @settings[:code_mode]
    @runtime_hooks = build_runtime_hooks(@settings[:runtime_hooks])

    @startup_phase = "instrumentation"
    subscribe_instrumentation(@settings[:instrumentation_subscribers])
    emit_startup(:runtime_start)

    @startup_phase = "loader"
    @loader = @settings[:loader] || Support::Loader.new(root: @root)
    loader.setup
    loader.eager_load

    @startup_phase = "model_resolver"
    @invocation_class = @settings[:invocation] || Invocation
    @model_resolver = @settings.fetch(:model_resolver)
    @default_model = @settings.fetch(:default_model, "default").to_s
    @model_operations = ModelOperations.new(model_resolver:)

    @startup_phase = "session_store"
    @session_store = build_session_store(@settings[:session_store])
    @session_actor = @settings[:session_actor]

    @startup_phase = "prompts"
    @prompt_paths = build_lookup_paths(:prompt_paths)
    @skill_paths = build_lookup_paths(:skill_paths)

    @startup_phase = "agent_factory"
    @agent_factory = AgentFactory.new(
      runtime: self,
      prompt_paths: @prompt_paths,
      resolve_agent: method(:resolve_agent_class)
    )

    @startup_phase = "complete"
    emit_startup(:runtime_stop, outcome: "ready")
    report_startup(status: "ready")
  rescue => error
    unless @startup_reported
      report_startup(status: "starting")
      @startup_reported = true
    end
    emit_startup(:runtime_stop, outcome: "failed", error:)
    Instrumentation.flush
    report_startup(status: "failed", error:)
    raise
  end
end

Instance Attribute Details

#code_mode_configurationObject (readonly)

Default code-mode declaration for enabled agents.



92
93
94
# File 'lib/little_ghost/runtime.rb', line 92

def code_mode_configuration
  @code_mode_configuration
end

#configurationObject (readonly)

Configuration object used to construct this Runtime.



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

def configuration
  @configuration
end

#loaderObject (readonly)

Loader used for conventional application definitions.



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

def loader
  @loader
end

#model_resolverObject (readonly)

Resolver that turns model roles and targets into executable Models.



84
85
86
# File 'lib/little_ghost/runtime.rb', line 84

def model_resolver
  @model_resolver
end

#prompt_pathsObject (readonly)

Ordered directories searched for prompt templates.



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

def prompt_paths
  @prompt_paths
end

#rootObject (readonly)

Canonical application root.



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

def root
  @root
end

#runtime_hooksObject (readonly)

Runtime hooks called around request and session preparation.



94
95
96
# File 'lib/little_ghost/runtime.rb', line 94

def runtime_hooks
  @runtime_hooks
end

#sandbox_declarationObject (readonly)

Configured Sandbox provider symbol, callable, or declaration.



90
91
92
# File 'lib/little_ghost/runtime.rb', line 90

def sandbox_declaration
  @sandbox_declaration
end

#session_storeObject (readonly)

Shared store used to open per-Run Sessions.



86
87
88
# File 'lib/little_ghost/runtime.rb', line 86

def session_store
  @session_store
end

#settingsObject (readonly)

Settings snapshot used by new Runs.



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

def settings
  @settings
end

#skill_pathsObject (readonly)

Ordered directories searched for skill definitions.



80
81
82
# File 'lib/little_ghost/runtime.rb', line 80

def skill_paths
  @skill_paths
end

#skill_resource_rootObject (readonly)

Root used for skill-owned resources, when configured.



82
83
84
# File 'lib/little_ghost/runtime.rb', line 82

def skill_resource_root
  @skill_resource_root
end

#task_runnerObject (readonly)

:nodoc:



95
96
97
# File 'lib/little_ghost/runtime.rb', line 95

def task_runner
  @task_runner
end

#workspace_declarationObject (readonly)

Configured Workspace provider symbol, callable, or declaration.



88
89
90
# File 'lib/little_ghost/runtime.rb', line 88

def workspace_declaration
  @workspace_declaration
end

Instance Method Details

#build(**overrides) ⇒ Object

Creates a sibling runtime with explicit setting overrides.



172
173
174
175
176
177
178
179
180
# File 'lib/little_ghost/runtime.rb', line 172

def build(**overrides)
  values = @settings.merge(overrides)
  values[:root] = canonical_application_root(values.fetch(:root))
  values[:loader] = loader unless overrides.key?(:loader) || overrides.key?(:root)
  self.class.new(
    configuration:,
    settings: values
  )
end

#build_agent(agent_class_or_name, run:, model: nil, tools: [], agent_path: Subagents::AgentPath::ROOT, agent_stream_path: []) ⇒ Object

:nodoc:



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/little_ghost/runtime.rb', line 293

def build_agent(
  agent_class_or_name,
  run:,
  model: nil,
  tools: [],
  agent_path: Subagents::AgentPath::ROOT,
  agent_stream_path: []
)
  agent_class_or_name = agent_class_or_name.definition if agent_class_or_name.is_a?(AgentBuilder)
  if agent_class_or_name.is_a?(AssemblyDefinition)
    unless agent_class_or_name.kind == :agent
      raise ConfigurationError, "agent definition must have kind :agent"
    end
    agent_class_or_name = agent_class_or_name.implementation
  end
  @agent_factory.build(agent_class_or_name, run:, model:, tools:, agent_path:, agent_stream_path:)
end

#build_assembly(assembly_class_or_name, run:, **options) ⇒ Object

Builds an Agent through AgentFactory or constructs another Assembly for run.



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/little_ghost/runtime.rb', line 312

def build_assembly(assembly_class_or_name, run:, **options) # :nodoc:
  agent_stream_path = options.delete(:agent_stream_path) || []
  if assembly_class_or_name.is_a?(AssemblyBuilder)
    assembly_class_or_name = assembly_class_or_name.definition
  end
  if assembly_class_or_name.is_a?(Class) && assembly_class_or_name <= Assembly
    assembly_class_or_name = assembly_class_or_name.definition
  end
  if assembly_class_or_name.is_a?(AssemblyDefinition)
    return build_agent(assembly_class_or_name, run:, agent_stream_path:, **options) if assembly_class_or_name.kind == :agent
    raise ArgumentError, "composite assembly definitions do not accept agent build options" unless options.empty?

    return assembly_class_or_name.implementation.new(run:, runtime: self).bind_agent_stream_path(agent_stream_path)
  end
  if !assembly_class_or_name.is_a?(Class) || assembly_class_or_name <= Agent
    return build_agent(assembly_class_or_name, run:, agent_stream_path:, **options)
  end
  unless assembly_class_or_name <= Assembly
    raise ConfigurationError, "assembly must inherit from LittleGhost::Assembly"
  end
  unless options.empty?
    raise ArgumentError, "composite assemblies do not accept agent build options"
  end

  assembly_class_or_name.new(run:, runtime: self).bind_agent_stream_path(agent_stream_path)
end

#build_run(payload, 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) ⇒ Object

Creates a Run that owns any workspace and sandbox built for the request.

include_agent_events_by_default is trusted stream policy for the Run returned by this build. It applies only when the Invocation omits include_agent_events and must not be forwarded to auxiliary Runs built while preparing the request.



215
216
217
218
219
220
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
249
250
251
252
253
254
255
# File 'lib/little_ghost/runtime.rb', line 215

def build_run(
  payload,
  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
  agent_class ||= entrypoint_class if entrypoint_class <= Agent
  owned_resources = []
  invocation = parse(payload)
  workspace ||= build_workspace(invocation:).tap { |resource| owned_resources << resource }
  sandbox ||= build_sandbox(workspace:, invocation:).tap { |resource| owned_resources << resource }
  run = Run.new(
    invocation:,
    runtime: self,
    agent_class:,
    entrypoint_class:,
    execution_class:,
    cancellation_token:,
    workspace:,
    sandbox:,
    include_agent_events_by_default:
  )
  owned_resources.each { |resource| run.register(resource) }
  prepare_run(run)
rescue
  if run
    run.close
  else
    close_resources(owned_resources)
  end
  raise
end

#build_sandbox(workspace:, invocation: nil) ⇒ Object

Instantiates the configured sandbox around workspace, or an unrestricted sandbox by default.



277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/little_ghost/runtime.rb', line 277

def build_sandbox(workspace:, invocation: nil)
  declaration = sandbox_declaration
  return Sandboxes::Unrestricted.new(workspace:) unless declaration

  provider, options = component_provider(declaration)
  provider = Sandbox.resolve_provider(provider) if provider.is_a?(Symbol)
  policy_options = options.slice(*Sandbox::Policy::COMMON_KEYS)
  policy_options.each_key { |key| options.delete(key) }
  policy_value = options.delete(:policy)
  if policy_value || !policy_options.empty?
    options[:policy] = Sandbox::Policy.coerce(policy_value, **policy_options)
  end
  build_component(provider, options, runtime: self, invocation:, workspace:)
end

#build_workspace(invocation: nil) ⇒ Object

Instantiates the configured workspace, or a root-scoped Workspace by default.



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/little_ghost/runtime.rb', line 258

def build_workspace(invocation: nil)
  declaration = workspace_declaration
  unless declaration
    paths = artifacts_enabled? ? {artifacts: "artifacts"} : {}
    return Workspace.new(root: root, paths:)
  end

  provider, options = component_provider(declaration)
  provider = Workspace.resolve_provider(provider) if provider.is_a?(Symbol)
  options[:root] = resolve_component_path(options[:root]) if options.key?(:root)
  options[:root] ||= root.to_s if provider == Workspace
  if artifacts_enabled? && provider == Workspace
    options[:paths] = {artifacts: "artifacts"}.merge(options.fetch(:paths, {}))
  end
  build_component(provider, options, runtime: self, invocation:)
end

#default_error_message(error, _run) ⇒ Object

:nodoc:



416
417
418
419
420
421
422
423
424
425
# File 'lib/little_ghost/runtime.rb', line 416

def default_error_message(error, _run) # :nodoc:
  return error.message if error.is_a?(UnsupportedInputError)
  return error.message if error.is_a?(ToolLoopError)
  return "The model reached its output limit before completing a response. Please retry with a narrower request." if error.is_a?(OutputLimitError)
  if error.is_a?(MalformedToolCallError)
    return "The model returned an invalid tool call before completing the response. Please retry with a narrower request."
  end

  "I hit an error while generating a response. Please retry."
end

#embed(**arguments) ⇒ Object

:call-seq:

embed(model:, inputs:, settings: {}, limits: {}, cancellation_token: Support::CancellationToken.new, deadline: nil) -> Embeddings::Response

Embeds text through this Runtime's model resolver.

Returns an Embeddings::Response without creating a Run or invoking runtime hooks. See LittleGhost.embed for the operation contract.



205
206
207
# File 'lib/little_ghost/runtime.rb', line 205

def embed(**arguments)
  @model_operations.embed(**arguments)
end

#error_message(error, run) ⇒ Object

:nodoc:



407
408
409
410
411
412
413
414
# File 'lib/little_ghost/runtime.rb', line 407

def error_message(error, run) # :nodoc:
  runtime_hooks.each do |hook|
    message = hook.error_message(error, run)
    return message if message
  end

  default_error_message(error, run)
end

#generate(**arguments) ⇒ Object

:call-seq:

generate(model:, messages:, result_schema: nil, settings: {}, structured_result_repair_attempts: 1, cancellation_token: Support::CancellationToken.new, deadline: nil) -> RunResult

Generates one response through this Runtime's model resolver.

Returns a RunResult without creating a Run or invoking runtime hooks. See LittleGhost.generate for the operation contract.



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

def generate(**arguments)
  @model_operations.generate(**arguments)
end

#model_for(agent_class, run) ⇒ Object

:nodoc:



344
345
346
347
# File 'lib/little_ghost/runtime.rb', line 344

def model_for(agent_class, run) # :nodoc:
  selection = agent_class.model_selection(run.invocation) || @default_model
  model_resolver.resolve(selection, invocation: run.invocation, context: run)
end

#open_session(run) ⇒ Object

:nodoc:



349
350
351
352
353
354
355
356
# File 'lib/little_ghost/runtime.rb', line 349

def open_session(run) # :nodoc:
  Session.new(
    id: run.invocation.session_id,
    actor_id: session_actor_for(run.invocation),
    store: session_store,
    operation_id: run.operation_id
  )
end

#open_subagent_session(run, conversation_id) ⇒ Object

:nodoc:



384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/little_ghost/runtime.rb', line 384

def open_subagent_session(run, conversation_id) # :nodoc:
  parent_link = Subagents::Manager.parent_link(run.session)
  Session.new(
    id: Subagents::Manager.conversation_session_id(conversation_id),
    actor_id: session_actor_for(run.invocation),
    store: session_store,
    operation_id: run.operation_id,
    metadata: {
      "little_ghost_kind" => "subagent_conversation",
      "little_ghost_parent_link" => parent_link,
      "little_ghost_conversation_id" => conversation_id
    }
  )
end

#parse(payload) ⇒ Object

Coerces an application payload into the configured Invocation class.



183
184
185
# File 'lib/little_ghost/runtime.rb', line 183

def parse(payload)
  payload.is_a?(@invocation_class) ? payload : @invocation_class.new(payload)
end

#prepare_execution(run) ⇒ Object

:nodoc:



373
374
375
376
# File 'lib/little_ghost/runtime.rb', line 373

def prepare_execution(run) # :nodoc:
  runtime_hooks.each { |hook| hook.prepare_execution(run) }
  run
end

#prepare_interjection(run, payload) ⇒ Object

:nodoc:



378
379
380
381
382
# File 'lib/little_ghost/runtime.rb', line 378

def prepare_interjection(run, payload) # :nodoc:
  runtime_hooks.reduce(payload) do |prepared, hook|
    hook.prepare_interjection(run, prepared)
  end
end

#prepare_run(run) ⇒ Object

:nodoc:



368
369
370
371
# File 'lib/little_ghost/runtime.rb', line 368

def prepare_run(run) # :nodoc:
  runtime_hooks.each { |hook| hook.prepare_run(run) }
  run
end

#resolve_agent(value) ⇒ Object

:nodoc:



427
428
429
# File 'lib/little_ghost/runtime.rb', line 427

def resolve_agent(value) # :nodoc:
  resolve_agent_class(value)
end

#service_nameObject

The stable service name attached to runtime telemetry.



340
341
342
# File 'lib/little_ghost/runtime.rb', line 340

def service_name
  @settings&.[](:service_name) || default_service_name
end

#session_actor_for(invocation) ⇒ Object

:nodoc:



399
400
401
# File 'lib/little_ghost/runtime.rb', line 399

def session_actor_for(invocation) # :nodoc:
  @session_actor ? @session_actor.call(invocation) : invocation.actor_id
end

#session_history(run, session, fallback:) ⇒ Object

:nodoc:



358
359
360
361
362
363
364
365
366
# File 'lib/little_ghost/runtime.rb', line 358

def session_history(run, session, fallback:) # :nodoc:
  stored = session.history
  runtime_hooks.each do |hook|
    history = hook.session_history(run, stored:, fallback:)
    return normalize_history(history) unless history.nil?
  end

  session.history(fallback:)
end

#template_locals(run:, agent:) ⇒ Object

:nodoc:



403
404
405
# File 'lib/little_ghost/runtime.rb', line 403

def template_locals(run:, agent:) # :nodoc:
  {invocation: run.invocation, run:, agent:}.merge(agent.prompt_locals)
end