Class: LittleGhost::Runtime
- Inherits:
-
Object
- Object
- LittleGhost::Runtime
- Defined in:
- lib/little_ghost/runtime.rb,
lib/little_ghost/runtime/hook.rb
Overview
Prepare the shared services that assemblies use across many runs. A runtime owns model resolution, loading, persistence, lookup paths, hooks, and resource factories for one Ruby setup.
configuration = LittleGhost::Configuration.new(
root: Dir.pwd,
providers: {
openai: {adapter: :openai, api_key: ENV.fetch("OPENAI_API_KEY")}
},
models: {customer_support: {target: "openai:gpt-5.6-luna"}},
default_model: "customer_support",
service_name: "support-api"
)
runtime = LittleGhost::Runtime.new(configuration: configuration)
runtime.service_name # => "support-api"
runtime.root == Pathname.new(File.realpath(Dir.pwd)) # => true
Without explicit settings, construction canonicalizes the root, loads
config/little_ghost.rb once through the Configuration, snapshots settings,
configures instrumentation, eager-loads application constants, and builds the
selected model resolver and session store. Supplying settings is the
lower-level path used to create a sibling runtime from an existing snapshot.
Reuse a runtime across runs. build_run creates any missing workspace and
sandbox, transfers ownership only after both are built, and closes partial
resources if construction fails. build creates a sibling with explicit
overrides and reuses the loader only when the application root is unchanged.
Startup emits structured lifecycle instrumentation; a failed phase emits a failure event, flushes instrumentation, and re-raises the original exception. Session actor resolution must use trusted authenticated identity for tenant isolation. The default UnrestrictedSandbox is convenient application plumbing, not a security boundary for untrusted work.
Defined Under Namespace
Classes: Hook
Instance Attribute Summary collapse
-
#configuration ⇒ Object
readonly
The snapshotted setup and materialized services used by new runs.
-
#loader ⇒ Object
readonly
The snapshotted setup and materialized services used by new runs.
-
#model_resolver ⇒ Object
readonly
The snapshotted setup and materialized services used by new runs.
-
#prompt_paths ⇒ Object
readonly
The snapshotted setup and materialized services used by new runs.
-
#root ⇒ Object
readonly
The snapshotted setup and materialized services used by new runs.
-
#runtime_hooks ⇒ Object
readonly
The snapshotted setup and materialized services used by new runs.
-
#sandbox_class ⇒ Object
readonly
The snapshotted setup and materialized services used by new runs.
-
#session_store ⇒ Object
readonly
The snapshotted setup and materialized services used by new runs.
-
#settings ⇒ Object
readonly
The snapshotted setup and materialized services used by new runs.
-
#skill_paths ⇒ Object
readonly
The snapshotted setup and materialized services used by new runs.
-
#skill_resource_root ⇒ Object
readonly
The snapshotted setup and materialized services used by new runs.
-
#workspace_class ⇒ Object
readonly
The snapshotted setup and materialized services used by new runs.
Instance Method Summary collapse
-
#build(**overrides) ⇒ Object
Creates a sibling runtime with explicit setting overrides.
-
#build_agent(agent_class_or_name, run:, model: nil, tools: [], agent_path: Subagents::AgentPath::ROOT) ⇒ Object
:nodoc:.
-
#build_assembly(assembly_class_or_name, run:, **options) ⇒ Object
Builds an Agent through AgentFactory or constructs another Assembly for
run. -
#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 and transfers ownership of newly created workspace and sandbox resources to it.
-
#build_sandbox(workspace:) ⇒ Object
Instantiates the configured sandbox around
workspace, or an unrestricted sandbox by default. -
#build_workspace ⇒ Object
Instantiates the configured workspace, or a root-scoped Workspace by default.
-
#default_error_message(error, _run) ⇒ Object
:nodoc:.
-
#error_message(error, run) ⇒ Object
:nodoc:.
-
#initialize(configuration:, settings: nil) ⇒ Runtime
constructor
Starts a runtime from
configurationor an existing settings snapshot. -
#model_for(agent_class, run) ⇒ Object
:nodoc:.
-
#open_session(run) ⇒ Object
:nodoc:.
-
#open_subagent_session(run, conversation_id) ⇒ Object
:nodoc:.
-
#parse(payload) ⇒ Object
Coerces an application payload into the configured Invocation class.
-
#prepare_interruption(run, payload) ⇒ Object
:nodoc:.
-
#prepare_run(run) ⇒ Object
:nodoc:.
-
#resolve_agent(value) ⇒ Object
:nodoc:.
-
#service_name ⇒ Object
The low-cardinality service name attached to runtime telemetry.
-
#session_actor_for(invocation) ⇒ Object
:nodoc:.
-
#session_history(run, session, fallback:) ⇒ Object
:nodoc:.
-
#template_locals(run:, agent:) ⇒ Object
:nodoc:.
Constructor Details
#initialize(configuration:, settings: nil) ⇒ Runtime
Starts a runtime from configuration or an existing settings snapshot.
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 76 77 78 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 114 |
# File 'lib/little_ghost/runtime.rb', line 48 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
#configuration ⇒ Object (readonly)
The snapshotted setup and materialized services used by new runs.
43 44 45 |
# File 'lib/little_ghost/runtime.rb', line 43 def configuration @configuration end |
#loader ⇒ Object (readonly)
The snapshotted setup and materialized services used by new runs.
43 44 45 |
# File 'lib/little_ghost/runtime.rb', line 43 def loader @loader end |
#model_resolver ⇒ Object (readonly)
The snapshotted setup and materialized services used by new runs.
43 44 45 |
# File 'lib/little_ghost/runtime.rb', line 43 def model_resolver @model_resolver end |
#prompt_paths ⇒ Object (readonly)
The snapshotted setup and materialized services used by new runs.
43 44 45 |
# File 'lib/little_ghost/runtime.rb', line 43 def prompt_paths @prompt_paths end |
#root ⇒ Object (readonly)
The snapshotted setup and materialized services used by new runs.
43 44 45 |
# File 'lib/little_ghost/runtime.rb', line 43 def root @root end |
#runtime_hooks ⇒ Object (readonly)
The snapshotted setup and materialized services used by new runs.
43 44 45 |
# File 'lib/little_ghost/runtime.rb', line 43 def runtime_hooks @runtime_hooks end |
#sandbox_class ⇒ Object (readonly)
The snapshotted setup and materialized services used by new runs.
43 44 45 |
# File 'lib/little_ghost/runtime.rb', line 43 def sandbox_class @sandbox_class end |
#session_store ⇒ Object (readonly)
The snapshotted setup and materialized services used by new runs.
43 44 45 |
# File 'lib/little_ghost/runtime.rb', line 43 def session_store @session_store end |
#settings ⇒ Object (readonly)
The snapshotted setup and materialized services used by new runs.
43 44 45 |
# File 'lib/little_ghost/runtime.rb', line 43 def settings @settings end |
#skill_paths ⇒ Object (readonly)
The snapshotted setup and materialized services used by new runs.
43 44 45 |
# File 'lib/little_ghost/runtime.rb', line 43 def skill_paths @skill_paths end |
#skill_resource_root ⇒ Object (readonly)
The snapshotted setup and materialized services used by new runs.
43 44 45 |
# File 'lib/little_ghost/runtime.rb', line 43 def skill_resource_root @skill_resource_root end |
#workspace_class ⇒ Object (readonly)
The snapshotted setup and materialized services used by new runs.
43 44 45 |
# File 'lib/little_ghost/runtime.rb', line 43 def workspace_class @workspace_class end |
Instance Method Details
#build(**overrides) ⇒ Object
Creates a sibling runtime with explicit setting overrides.
117 118 119 120 121 122 123 124 125 |
# File 'lib/little_ghost/runtime.rb', line 117 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:
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/little_ghost/runtime.rb', line 189 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.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/little_ghost/runtime.rb', line 207 def build_assembly(assembly_class_or_name, run:, **) # :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:, **) if assembly_class_or_name.kind == :agent raise ArgumentError, "composite assembly definitions do not accept agent build options" unless .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:, **) end unless assembly_class_or_name <= Assembly raise ConfigurationError, "assembly must inherit from LittleGhost::Assembly" end unless .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 and transfers ownership of newly created workspace and sandbox resources to it.
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 134 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.
182 183 184 185 186 |
# File 'lib/little_ghost/runtime.rb', line 182 def build_sandbox(workspace:) return sandbox_class.new(workspace:) if sandbox_class UnrestrictedSandbox.new(workspace:) end |
#build_workspace ⇒ Object
Instantiates the configured workspace, or a root-scoped Workspace by default.
174 175 176 177 178 |
# File 'lib/little_ghost/runtime.rb', line 174 def build_workspace return workspace_class.new if workspace_class Workspace.new(root: root) end |
#default_error_message(error, _run) ⇒ Object
:nodoc:
305 306 307 308 309 310 311 312 313 314 |
# File 'lib/little_ghost/runtime.rb', line 305 def (error, _run) # :nodoc: return error. if error.is_a?(UnsupportedInputError) return error. 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:
296 297 298 299 300 301 302 303 |
# File 'lib/little_ghost/runtime.rb', line 296 def (error, run) # :nodoc: runtime_hooks.each do |hook| = hook.(error, run) return if end (error, run) end |
#model_for(agent_class, run) ⇒ Object
:nodoc:
238 239 240 241 |
# File 'lib/little_ghost/runtime.rb', line 238 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:
243 244 245 246 247 248 249 250 |
# File 'lib/little_ghost/runtime.rb', line 243 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:
273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/little_ghost/runtime.rb', line 273 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.
128 129 130 |
# File 'lib/little_ghost/runtime.rb', line 128 def parse(payload) payload.is_a?(@invocation_class) ? payload : @invocation_class.new(payload) end |
#prepare_interruption(run, payload) ⇒ Object
:nodoc:
267 268 269 270 271 |
# File 'lib/little_ghost/runtime.rb', line 267 def prepare_interruption(run, payload) # :nodoc: runtime_hooks.reduce(payload) do |prepared, hook| hook.prepare_interruption(run, prepared) end end |
#prepare_run(run) ⇒ Object
:nodoc:
262 263 264 265 |
# File 'lib/little_ghost/runtime.rb', line 262 def prepare_run(run) # :nodoc: runtime_hooks.each { |hook| hook.prepare_run(run) } run end |
#resolve_agent(value) ⇒ Object
:nodoc:
316 317 318 |
# File 'lib/little_ghost/runtime.rb', line 316 def resolve_agent(value) # :nodoc: resolve_agent_class(value) end |
#service_name ⇒ Object
The low-cardinality service name attached to runtime telemetry.
234 235 236 |
# File 'lib/little_ghost/runtime.rb', line 234 def service_name @settings&.[](:service_name) || default_service_name end |
#session_actor_for(invocation) ⇒ Object
:nodoc:
288 289 290 |
# File 'lib/little_ghost/runtime.rb', line 288 def session_actor_for(invocation) # :nodoc: @session_actor ? @session_actor.call(invocation) : invocation.actor_id end |
#session_history(run, session, fallback:) ⇒ Object
:nodoc:
252 253 254 255 256 257 258 259 260 |
# File 'lib/little_ghost/runtime.rb', line 252 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:
292 293 294 |
# File 'lib/little_ghost/runtime.rb', line 292 def template_locals(run:, agent:) # :nodoc: {invocation: run.invocation, run:, agent:}.merge(agent.prompt_locals) end |