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.



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
170
171
# File 'lib/little_ghost/runtime.rb', line 99

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

    @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)
    @framework_prompts = FrameworkPrompts.new(paths: @prompt_paths)
    @skill_paths = build_lookup_paths(:skill_paths)
    @model_operations = ModelOperations.new(model_resolver:, framework_prompts:)

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



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

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

#framework_promptsObject (readonly)

:nodoc:



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

def framework_prompts
  @framework_prompts
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.



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

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.



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

def runtime_hooks
  @runtime_hooks
end

#sandbox_declarationObject (readonly)

Configured Sandbox provider symbol, callable, or declaration.



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

def sandbox_declaration
  @sandbox_declaration
end

#session_storeObject (readonly)

Shared store used to open per-Run Sessions.



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

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.



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

def skill_paths
  @skill_paths
end

#skill_resource_rootObject (readonly)

Root used for skill-owned resources, when configured.



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

def skill_resource_root
  @skill_resource_root
end

#task_runnerObject (readonly)

:nodoc:



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

def task_runner
  @task_runner
end

#workspace_declarationObject (readonly)

Configured Workspace provider symbol, callable, or declaration.



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

def workspace_declaration
  @workspace_declaration
end

Instance Method Details

#build(**overrides) ⇒ Object

Creates a sibling runtime with explicit setting overrides.



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

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:



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

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.



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

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_assembly_definition(assembly_class_or_name)
        .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.



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
256
257
# File 'lib/little_ghost/runtime.rb', line 217

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.



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

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.



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

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:



421
422
423
424
425
426
427
428
429
430
# File 'lib/little_ghost/runtime.rb', line 421

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.



207
208
209
# File 'lib/little_ghost/runtime.rb', line 207

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

#error_message(error, run) ⇒ Object

:nodoc:



412
413
414
415
416
417
418
419
# File 'lib/little_ghost/runtime.rb', line 412

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, template_paths: [], 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.



196
197
198
# File 'lib/little_ghost/runtime.rb', line 196

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

#model_for(agent_class, run) ⇒ Object

:nodoc:



349
350
351
352
# File 'lib/little_ghost/runtime.rb', line 349

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:



354
355
356
357
358
359
360
361
# File 'lib/little_ghost/runtime.rb', line 354

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:



389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/little_ghost/runtime.rb', line 389

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.



185
186
187
# File 'lib/little_ghost/runtime.rb', line 185

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

#prepare_execution(run) ⇒ Object

:nodoc:



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

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

#prepare_interjection(run, payload) ⇒ Object

:nodoc:



383
384
385
386
387
# File 'lib/little_ghost/runtime.rb', line 383

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:



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

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

#resolve_agent(value) ⇒ Object

:nodoc:



432
433
434
# File 'lib/little_ghost/runtime.rb', line 432

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

#service_nameObject

The stable service name attached to runtime telemetry.



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

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

#session_actor_for(invocation) ⇒ Object

:nodoc:



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

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

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

:nodoc:



363
364
365
366
367
368
369
370
371
# File 'lib/little_ghost/runtime.rb', line 363

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:



408
409
410
# File 'lib/little_ghost/runtime.rb', line 408

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