Class: LittleGhost::Runtime

Inherits:
Object
  • Object
show all
Defined in:
lib/little_ghost/runtime.rb,
lib/little_ghost/runtime/hook.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 its own bound participants and Tools. By default, Runtime also creates a workspace and sandbox owned by that Run. Existing workspace or sandbox instances passed by the application remain caller-owned. Other supplied objects may receive calls from several threads, so custom stores, resolvers, hooks, subscribers, providers, and resource factories must be thread-safe. One SessionStore instance serializes calls sharing a Session; multi-process deployments need coordination provided by their store.

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 UnrestrictedSandbox uses host permissions and is not a security boundary for untrusted work.

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

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.



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

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
    report_startup(status: "starting")
    @startup_reported = true
    @root = canonical_application_root(@settings.fetch(:root))
    @skill_resource_root = @settings[:skill_resource_root]
    @workspace_class = @settings.fetch(:workspace)
    @sandbox_class = @settings.fetch(:sandbox)
    @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)
    @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

#configurationObject (readonly)

Configuration object used to construct this Runtime.



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

def configuration
  @configuration
end

#loaderObject (readonly)

Loader used for conventional application definitions.



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

def loader
  @loader
end

#model_resolverObject (readonly)

Resolver that turns model roles and targets into executable Models.



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

def model_resolver
  @model_resolver
end

#prompt_pathsObject (readonly)

Ordered directories searched for prompt templates.



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

def prompt_paths
  @prompt_paths
end

#rootObject (readonly)

Canonical application root.



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

def root
  @root
end

#runtime_hooksObject (readonly)

Runtime hooks called around request and session preparation.



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

def runtime_hooks
  @runtime_hooks
end

#sandbox_classObject (readonly)

Configured Sandbox implementation, or nil for the default.



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

def sandbox_class
  @sandbox_class
end

#session_storeObject (readonly)

Shared store used to open per-Run Sessions.



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

def session_store
  @session_store
end

#settingsObject (readonly)

Settings snapshot used by new Runs.



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

def settings
  @settings
end

#skill_pathsObject (readonly)

Ordered directories searched for skill definitions.



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

def skill_paths
  @skill_paths
end

#skill_resource_rootObject (readonly)

Root used for skill-owned resources, when configured.



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

def skill_resource_root
  @skill_resource_root
end

#workspace_classObject (readonly)

Configured Workspace implementation, or nil for the default.



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

def workspace_class
  @workspace_class
end

Instance Method Details

#build(**overrides) ⇒ Object

Creates a sibling runtime with explicit setting overrides.



158
159
160
161
162
163
164
165
166
# File 'lib/little_ghost/runtime.rb', line 158

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

:nodoc:



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/little_ghost/runtime.rb', line 229

def build_agent(
  agent_class_or_name,
  run:,
  model: nil,
  tools: [],
  agent_path: Subagents::AgentPath::ROOT
)
  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:)
end

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

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



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/little_ghost/runtime.rb', line 247

def build_assembly(assembly_class_or_name, run:, **options) # :nodoc:
  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:, **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)
  end
  if !assembly_class_or_name.is_a?(Class) || assembly_class_or_name <= Agent
    return build_agent(assembly_class_or_name, run:, **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)
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) ⇒ Object

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



174
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
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/little_ghost/runtime.rb', line 174

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
)
  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 = []
  workspace ||= build_workspace.tap { |resource| owned_resources << resource }
  sandbox ||= build_sandbox(workspace:).tap { |resource| owned_resources << resource }
  run = Run.new(
    invocation: parse(payload),
    runtime: self,
    agent_class:,
    entrypoint_class:,
    execution_class:,
    cancellation_token:,
    workspace:,
    sandbox:
  )
  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:) ⇒ Object

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



222
223
224
225
226
# File 'lib/little_ghost/runtime.rb', line 222

def build_sandbox(workspace:)
  return sandbox_class.new(workspace:) if sandbox_class

  UnrestrictedSandbox.new(workspace:)
end

#build_workspaceObject

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



214
215
216
217
218
# File 'lib/little_ghost/runtime.rb', line 214

def build_workspace
  return workspace_class.new if workspace_class

  Workspace.new(root: root)
end

#default_error_message(error, _run) ⇒ Object

:nodoc:



345
346
347
348
349
350
351
352
353
354
# File 'lib/little_ghost/runtime.rb', line 345

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

  "Agent failed: #{error.class}"
end

#error_message(error, run) ⇒ Object

:nodoc:



336
337
338
339
340
341
342
343
# File 'lib/little_ghost/runtime.rb', line 336

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

#model_for(agent_class, run) ⇒ Object

:nodoc:



278
279
280
281
# File 'lib/little_ghost/runtime.rb', line 278

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:



283
284
285
286
287
288
289
290
# File 'lib/little_ghost/runtime.rb', line 283

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:



313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/little_ghost/runtime.rb', line 313

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.



169
170
171
# File 'lib/little_ghost/runtime.rb', line 169

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

#prepare_interjection(run, payload) ⇒ Object

:nodoc:



307
308
309
310
311
# File 'lib/little_ghost/runtime.rb', line 307

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:



302
303
304
305
# File 'lib/little_ghost/runtime.rb', line 302

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

#resolve_agent(value) ⇒ Object

:nodoc:



356
357
358
# File 'lib/little_ghost/runtime.rb', line 356

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

#service_nameObject

The stable service name attached to runtime telemetry.



274
275
276
# File 'lib/little_ghost/runtime.rb', line 274

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

#session_actor_for(invocation) ⇒ Object

:nodoc:



328
329
330
# File 'lib/little_ghost/runtime.rb', line 328

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

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

:nodoc:



292
293
294
295
296
297
298
299
300
# File 'lib/little_ghost/runtime.rb', line 292

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:



332
333
334
# File 'lib/little_ghost/runtime.rb', line 332

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