Class: LittleGhost::Assembly
- Inherits:
-
Object
- Object
- LittleGhost::Assembly
- Extended by:
- Support::ClassAttributes
- Defined in:
- lib/little_ghost/assembly.rb,
lib/little_ghost/assembly_execution.rb
Overview
Gives one agent or a coordinated group the same callable entrypoint.
An assembly is anything callers can invoke like one Agent. An Agent is the
smallest assembly because it owns one model loop. Workflow, Swarm, and Graph
subclasses coordinate several participants while preserving the same
ask, stream_ask, call, and stream interface.
agent_run = CustomerSupportAgent.ask("Why is my transfer pending?")
graph_run = SupportFlowGraph.ask("Why is my transfer pending?")
agent_run.response
graph_run.response
Named subclasses are the usual form. to_builder creates a mutable dynamic
definition seeded by the class, while definition returns the immutable
snapshot used for one execution. Composite results expose Assembly::Step
records through RunResult#trajectory.
A standalone assembly owns a top-level Run. An assembly built by a Runtime participates in the existing run and returns a RunResult. Applications normally subclass Agent, Workflow, Swarm, or Graph rather than Assembly directly. Standalone calls automatically reuse the active Configuration's shared Runtime while keeping each Run and its resources independent.
What each calling form returns
[CustomerSupportAgent.ask(...)]
A named class creates and returns a top-level Run.
[CustomerSupportAgent.new(runtime: runtime).ask(...)]
A standalone instance also creates and returns a top-level Run.
[runtime.build_assembly(..., run: run).call(...)]
A participant already bound to a Run returns its child RunResult.
[stream_ask(...).each { |event| ... }]
A standalone stream returns its top-level Run after enumeration. A
run-scoped stream ends with an invocation_stop event carrying RunResult.
Defined Under Namespace
Classes: Attempt, Step, StepExecution, Trajectory
Constant Summary collapse
- MAX_STEP_OUTPUT_BYTES =
:nodoc:
64 * 1024
- MAX_STEP_EVENTS =
:nodoc:
10_000- MAX_STEP_EVENT_BYTES =
:nodoc:
10 * 1024 * 1024
Instance Attribute Summary collapse
-
#run ⇒ Object
readonly
The owning Run, or
nilfor a standalone entrypoint. -
#runtime ⇒ Object
readonly
Runtime used to resolve participants and build Runs.
-
#sandbox ⇒ Object
readonly
Sandbox supplied to this Assembly, when present.
-
#workspace ⇒ Object
readonly
Workspace supplied to this Assembly, when present.
Class Method Summary collapse
-
.ask(message, **options) ⇒ Object
Executes
messagethrough a fresh standalone assembly and returns its Run. -
.assembly_id(*values) ⇒ Object
:call-seq: assembly_id() -> String assembly_id(value) -> String.
-
.assembly_kind ⇒ Object
Returns
:agent,:workflow,:swarm,:graph, or:assembly. -
.definition ⇒ Object
Returns an immutable definition for this class.
-
.description(*values) ⇒ Object
:call-seq: description() -> String description(value) -> String.
-
.stream_ask(message, **options) ⇒ Object
Lazily streams
messagethrough a fresh standalone assembly. -
.to_builder ⇒ Object
Returns a mutable dynamic builder seeded by this class.
-
.validate_step_policy!(values) ⇒ Object
:nodoc:.
Instance Method Summary collapse
-
#as_tool(name: self.class.assembly_id, description: self.class.description, preserve_context: false) ⇒ Object
Exposes this assembly as a Tool instance.
-
#ask(message, **options) ⇒ Object
Runs
messageto completion. -
#build_run(payload) ⇒ Object
Builds the top-level Run used by a standalone assembly.
-
#call(input = nil, **options) ⇒ Object
Runs
inputto completion. -
#close ⇒ Object
Closes resources owned directly by this assembly.
-
#entrypoint_name ⇒ Object
:nodoc:.
-
#initialize(run: nil, runtime: nil, workspace: nil, sandbox: nil, standalone: run.nil?) ⇒ Assembly
constructor
:nodoc:.
-
#interject(message, **options) ⇒ Object
Adds an interjection to the single active leaf Agent.
-
#prompt_locals ⇒ Object
Additional prompt locals made available to child agents.
-
#start_execution(payload, &event_consumer) ⇒ Object
Starts
payloadon a supervised worker and returns an Execution. -
#stream_ask(message, **options) ⇒ Object
Lazily streams
messagethrough the standalone or run-scoped assembly.
Methods included from Support::ClassAttributes
Constructor Details
#initialize(run: nil, runtime: nil, workspace: nil, sandbox: nil, standalone: run.nil?) ⇒ Assembly
:nodoc:
148 149 150 151 152 153 154 155 156 157 |
# File 'lib/little_ghost/assembly.rb', line 148 def initialize(run: nil, runtime: nil, workspace: nil, sandbox: nil, standalone: run.nil?) # :nodoc: @run = run @runtime = runtime || run&.runtime || LittleGhost.runtime @workspace = workspace || (run.workspace if run&.respond_to?(:workspace)) @sandbox = sandbox || (run.sandbox if run&.respond_to?(:sandbox)) @standalone = standalone @assembly_mutex = Mutex.new @assembly_closed = false @active_assemblies = [] end |
Instance Attribute Details
#run ⇒ Object (readonly)
The owning Run, or nil for a standalone entrypoint.
140 141 142 |
# File 'lib/little_ghost/assembly.rb', line 140 def run @run end |
#runtime ⇒ Object (readonly)
Runtime used to resolve participants and build Runs.
142 143 144 |
# File 'lib/little_ghost/assembly.rb', line 142 def runtime @runtime end |
#sandbox ⇒ Object (readonly)
Sandbox supplied to this Assembly, when present.
146 147 148 |
# File 'lib/little_ghost/assembly.rb', line 146 def sandbox @sandbox end |
#workspace ⇒ Object (readonly)
Workspace supplied to this Assembly, when present.
144 145 146 |
# File 'lib/little_ghost/assembly.rb', line 144 def workspace @workspace end |
Class Method Details
.ask(message, **options) ⇒ Object
Executes message through a fresh standalone assembly and returns its Run.
options become Invocation fields. Common values include history,
context, settings, metadata, session_id, actor_id, and
deadline_at.
51 52 53 |
# File 'lib/little_ghost/assembly.rb', line 51 def ask(, **) definition.implementation.new.ask(, **) end |
.assembly_id(*values) ⇒ Object
:call-seq:
assembly_id() -> String
assembly_id(value) -> String
The stable identifier used for tools and telemetry. Named subclasses derive it from their underscored class name without their type suffix.
103 104 105 106 107 |
# File 'lib/little_ghost/assembly.rb', line 103 def assembly_id(*values) return assembly_id_value || default_assembly_id if values.empty? self.assembly_id_value = values.fetch(0).to_s end |
.assembly_kind ⇒ Object
Returns :agent, :workflow, :swarm, :graph, or :assembly.
121 122 123 124 125 126 127 128 |
# File 'lib/little_ghost/assembly.rb', line 121 def assembly_kind return :agent if defined?(Agent) && self <= Agent return :workflow if defined?(Workflow) && self <= Workflow return :swarm if defined?(Swarm) && self <= Swarm return :graph if defined?(Graph) && self <= Graph :assembly end |
.definition ⇒ Object
Returns an immutable definition for this class.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/little_ghost/assembly.rb', line 69 def definition if assembly_kind == :assembly implementation = dup implementation.assembly_id(assembly_id) implementation.description(description) implementation.freeze return AssemblyDefinition.new( kind: :assembly, assembly_id:, description:, implementation: ) end to_builder.definition end |
.description(*values) ⇒ Object
:call-seq:
description() -> String
description(value) -> String
The human-readable description used when exposing the assembly as a tool.
114 115 116 117 118 |
# File 'lib/little_ghost/assembly.rb', line 114 def description(*values) return description_value.to_s if values.empty? self.description_value = values.fetch(0).to_s end |
.stream_ask(message, **options) ⇒ Object
Lazily streams message through a fresh standalone assembly.
Enumeration yields StreamEvent objects and returns the terminal Run.
The same Invocation fields accepted by .ask may be supplied as options.
59 60 61 62 63 64 65 66 |
# File 'lib/little_ghost/assembly.rb', line 59 def stream_ask(, **) snapshot = definition stream = nil Enumerator.new do |events| stream ||= snapshot.implementation.new.stream_ask(, **) stream.each { |event| events << event } end end |
.to_builder ⇒ Object
Returns a mutable dynamic builder seeded by this class.
87 88 89 90 91 92 93 94 95 |
# File 'lib/little_ghost/assembly.rb', line 87 def to_builder builder_class = { agent: AgentBuilder, workflow: WorkflowBuilder, swarm: SwarmBuilder, graph: GraphBuilder }.fetch(assembly_kind) builder_class.new(base: self) end |
.validate_step_policy!(values) ⇒ Object
:nodoc:
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 |
# File 'lib/little_ghost/assembly_execution.rb', line 337 def validate_step_policy!(values) # :nodoc: values = values.compact retries = Integer(values.fetch(:retries, 0)) raise ArgumentError, "retries must be at least 0" if retries.negative? retry_on = Array(values[:retry_on]) if retries.positive? && retry_on.empty? raise ArgumentError, "retry_on is required when retries is greater than 0" end unless retry_on.all? { |error| error.is_a?(Class) && error <= Exception } raise ArgumentError, "retry_on must contain exception classes" end timeout = Float(values[:timeout]) if values[:timeout] retry_delay = Float(values.fetch(:retry_delay, 0)) raise ArgumentError, "timeout must be positive" if timeout && (!timeout.positive? || !timeout.finite?) raise ArgumentError, "retry_delay must be non-negative" if retry_delay.negative? || !retry_delay.finite? {retries:, retry_on: retry_on.freeze, timeout:, retry_delay:}.freeze end |
Instance Method Details
#as_tool(name: self.class.assembly_id, description: self.class.description, preserve_context: false) ⇒ Object
Exposes this assembly as a Tool instance.
By default, calls do not remember earlier conversation history. Set preserve_context: true to carry that history from one tool call to the next. This option does not control working state: every call receives the invoking Tool's current RunContext#state, which may include current request values or values restored from a Session. Nested tools must still authorize privileged work with current, application-established values.
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 258 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 287 |
# File 'lib/little_ghost/assembly.rb', line 229 def as_tool(name: self.class.assembly_id, description: self.class.description, preserve_context: false) assembly = self description = "Delegate a task to #{name}." if description.to_s.empty? mutex = Mutex.new retained_history = [] tool_class = Tool.define( name:, description:, input_schema: { type: "object", properties: {input: {type: "string"}}, required: ["input"], additionalProperties: false } ) do |input, context: nil| invocation = lambda do target = if assembly.is_a?(Agent) assembly elsif assembly.run assembly.runtime.build_assembly(assembly.class, run: assembly.run) else assembly.class.new(runtime: assembly.runtime) end = { history: preserve_context ? retained_history : [], context: context&.state || {}, cancellation_token: context&.cancellation_token || Support::CancellationToken.new, deadline: context&.deadline, parent_operation_id: assembly.run&.operation_id } if target.is_a?(Agent) [:interjection_metadata] = context&. [:interjection_ids] = context&.interjection_ids || [] end result = target.call(input.fetch("input"), **) if result.is_a?(Run) raise result.error if result.error result = result.result end raise ProtocolError, "assembly tool invocation did not return a result" unless result retained_history.replace(result..reject { || .role == :system }) if preserve_context result.structured? ? result.structured_result.value : result.text ensure target&.close unless target.equal?(assembly) end preserve_context ? mutex.synchronize(&invocation) : invocation.call end tool_class.define_method(:close) { assembly.close } tool_class.new(binding: Tool::Binding.new( agent: (self if is_a?(Agent)), run:, runtime:, model: (model if respond_to?(:model)), workspace:, sandbox: )) end |
#ask(message, **options) ⇒ Object
Runs message to completion.
A standalone instance returns its owning Run. A run-scoped instance returns the child RunResult.
199 200 201 |
# File 'lib/little_ghost/assembly.rb', line 199 def ask(, **) call(, **) end |
#build_run(payload) ⇒ Object
Builds the top-level Run used by a standalone assembly.
160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/little_ghost/assembly.rb', line 160 def build_run(payload) # :nodoc: payload = payload.dup if payload.is_a?(Hash) cancellation_token = if payload.is_a?(Hash) payload.delete(:cancellation_token) || payload.delete("cancellation_token") end source_class = self.class.respond_to?(:assembly_source_class) ? self.class.assembly_source_class : self.class = {entrypoint_class: source_class} [:execution_class] = self.class unless source_class.equal?(self.class) [:agent_class] = source_class if is_a?(Agent) [:cancellation_token] = cancellation_token if cancellation_token [:workspace] = workspace if workspace [:sandbox] = sandbox if sandbox runtime.build_run(payload, **) end |
#call(input = nil, **options) ⇒ Object
Runs input to completion.
A standalone assembly returns a Run. A run-scoped assembly returns its RunResult.
185 186 187 188 189 190 191 192 193 |
# File 'lib/little_ghost/assembly.rb', line 185 def call(input = nil, **) return build_run(entrypoint_payload(input, )).call if standalone? result = nil stream(input, **).each do |event| result = event.data[:result] if event.type == :invocation_stop end result end |
#close ⇒ Object
Closes resources owned directly by this assembly.
305 306 307 308 309 310 311 |
# File 'lib/little_ghost/assembly.rb', line 305 def close @assembly_mutex.synchronize do return if @assembly_closed @assembly_closed = true end end |
#entrypoint_name ⇒ Object
:nodoc:
313 |
# File 'lib/little_ghost/assembly.rb', line 313 def entrypoint_name = self.class.assembly_id # :nodoc: |
#interject(message, **options) ⇒ Object
Adds an interjection to the single active leaf Agent.
290 291 292 293 294 295 296 297 298 299 300 301 302 |
# File 'lib/little_ghost/assembly.rb', line 290 def interject(, **) child = @assembly_mutex.synchronize do active = @active_assemblies.dup if active.empty? raise AgentInterjectionError, "Assembly is not currently running" end if active.length > 1 raise AgentInterjectionError, "Assembly has multiple active participants; the interjection target is ambiguous" end active.first end child.interject(, **) end |
#prompt_locals ⇒ Object
Additional prompt locals made available to child agents.
316 |
# File 'lib/little_ghost/assembly.rb', line 316 def prompt_locals = {} |
#start_execution(payload, &event_consumer) ⇒ Object
Starts payload on a supervised worker and returns an Execution.
176 177 178 179 |
# File 'lib/little_ghost/assembly.rb', line 176 def start_execution(payload, &event_consumer) ensure_standalone! Execution.start(build_run(payload), &event_consumer) end |
#stream_ask(message, **options) ⇒ Object
Lazily streams message through the standalone or run-scoped assembly.
A standalone stream returns its terminal Run after enumeration. A
run-scoped stream finishes with an invocation_stop event containing its
RunResult.
208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/little_ghost/assembly.rb', line 208 def stream_ask(, **) if standalone? [:deadline_at] = .delete(:deadline) if .key?(:deadline) stream = nil return Enumerator.new do |events| stream ||= build_run(entrypoint_payload(, )).each stream.each { |event| events << event } end end stream(, **) end |