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
Applications normally subclass Agent, Workflow, Swarm, or Graph rather than Assembly directly. Each standalone call returns a top-level Run. Participants called inside another Assembly return a RunResult to their parent.
Advanced construction
Named subclasses are the usual form. to_builder creates a mutable
definition for applications that discover participants at runtime.
definition returns the fixed snapshot used by one execution. Composite
results record their steps in RunResult#trajectory.
Return values
[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
-
#agent_stream_path ⇒ Object
readonly
:nodoc:.
-
#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. -
#bind_agent_stream_path(path) ⇒ Object
:nodoc:.
-
#bind_assembly_definition(definition) ⇒ Object
:nodoc:.
-
#build_run(payload = nil, include_agent_events_by_default: false, **payload_options) ⇒ 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
payloadin the background 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:
156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/little_ghost/assembly.rb', line 156 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 @sandbox = sandbox || run&.sandbox @assembly_definition = nil @standalone = standalone @assembly_mutex = Mutex.new @assembly_closed = false @active_assemblies = [] @agent_stream_path = [].freeze end |
Instance Attribute Details
#agent_stream_path ⇒ Object (readonly)
:nodoc:
154 155 156 |
# File 'lib/little_ghost/assembly.rb', line 154 def agent_stream_path @agent_stream_path end |
#run ⇒ Object (readonly)
The owning Run, or nil for a standalone entrypoint.
147 148 149 |
# File 'lib/little_ghost/assembly.rb', line 147 def run @run end |
#runtime ⇒ Object (readonly)
Runtime used to resolve participants and build Runs.
149 150 151 |
# File 'lib/little_ghost/assembly.rb', line 149 def runtime @runtime end |
#sandbox ⇒ Object (readonly)
Sandbox supplied to this Assembly, when present.
153 154 155 |
# File 'lib/little_ghost/assembly.rb', line 153 def sandbox @sandbox end |
#workspace ⇒ Object (readonly)
Workspace supplied to this Assembly, when present.
151 152 153 |
# File 'lib/little_ghost/assembly.rb', line 151 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.
52 53 54 55 |
# File 'lib/little_ghost/assembly.rb', line 52 def ask(, **) snapshot = definition snapshot.implementation.new.bind_assembly_definition(snapshot).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.
110 111 112 113 114 |
# File 'lib/little_ghost/assembly.rb', line 110 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.
128 129 130 131 132 133 134 135 |
# File 'lib/little_ghost/assembly.rb', line 128 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.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/little_ghost/assembly.rb', line 76 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.
121 122 123 124 125 |
# File 'lib/little_ghost/assembly.rb', line 121 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.
Composite assemblies also emit an :agent_stream event for every
normalized event from every Agent in the run, including intermediate
and nested participants. Set include_agent_events: false to
keep only the ordinary public stream. A standalone Agent retains its
ordinary stream by default and accepts true to opt in.
66 67 68 69 70 71 72 73 |
# File 'lib/little_ghost/assembly.rb', line 66 def stream_ask(, **) snapshot = definition stream = nil Enumerator.new do |events| stream ||= snapshot.implementation.new.bind_assembly_definition(snapshot).stream_ask(, **) stream.each { |event| events << event } end end |
.to_builder ⇒ Object
Returns a mutable dynamic builder seeded by this class.
94 95 96 97 98 99 100 101 102 |
# File 'lib/little_ghost/assembly.rb', line 94 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:
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/little_ghost/assembly_execution.rb', line 352 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.
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 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/little_ghost/assembly.rb', line 259 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.send(:build_tool_assembly) 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: context&.agent_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.
225 226 227 |
# File 'lib/little_ghost/assembly.rb', line 225 def ask(, **) call(, **) end |
#bind_agent_stream_path(path) ⇒ Object
:nodoc:
169 170 171 172 |
# File 'lib/little_ghost/assembly.rb', line 169 def bind_agent_stream_path(path) # :nodoc: @agent_stream_path = Array(path).dup.freeze self end |
#bind_assembly_definition(definition) ⇒ Object
:nodoc:
174 175 176 177 |
# File 'lib/little_ghost/assembly.rb', line 174 def bind_assembly_definition(definition) # :nodoc: @assembly_definition = definition self end |
#build_run(payload = nil, include_agent_events_by_default: false, **payload_options) ⇒ Object
Builds the top-level Run used by a standalone assembly.
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/little_ghost/assembly.rb', line 180 def build_run(payload = nil, include_agent_events_by_default: false, **) # :nodoc: payload = entrypoint_payload(payload, ) unless .empty? 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 = run_entrypoint_class = {entrypoint_class: source_class} execution = @assembly_definition || self.class [:execution_class] = execution unless source_class.equal?(execution) [:agent_class] = source_class if is_a?(Agent) [:cancellation_token] = cancellation_token if cancellation_token [:workspace] = workspace if workspace [:sandbox] = sandbox if sandbox [:include_agent_events_by_default] = true if include_agent_events_by_default 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.
211 212 213 214 215 216 217 218 219 |
# File 'lib/little_ghost/assembly.rb', line 211 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.
335 336 337 338 339 340 341 |
# File 'lib/little_ghost/assembly.rb', line 335 def close @assembly_mutex.synchronize do return if @assembly_closed @assembly_closed = true end end |
#entrypoint_name ⇒ Object
:nodoc:
343 |
# File 'lib/little_ghost/assembly.rb', line 343 def entrypoint_name = self.class.assembly_id # :nodoc: |
#interject(message, **options) ⇒ Object
Adds an interjection to the single active leaf Agent.
320 321 322 323 324 325 326 327 328 329 330 331 332 |
# File 'lib/little_ghost/assembly.rb', line 320 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.
346 |
# File 'lib/little_ghost/assembly.rb', line 346 def prompt_locals = {} |
#start_execution(payload, &event_consumer) ⇒ Object
Starts payload in the background and returns an Execution.
Composite assemblies include contextual :agent_stream events in the
consumer by default. Set include_agent_events to false in payload
to keep only the ordinary public stream.
202 203 204 205 |
# File 'lib/little_ghost/assembly.rb', line 202 def start_execution(payload, &event_consumer) ensure_standalone! Execution.start(build_stream_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. A standalone composite Assembly receives contextual
:agent_stream events from every Agent in the Run by default and may set
include_agent_events: false to omit them. A standalone Agent may
set the option to true to include its contextual wrapper.
237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/little_ghost/assembly.rb', line 237 def stream_ask(, **) if standalone? [:deadline_at] = .delete(:deadline) if .key?(:deadline) payload = entrypoint_payload(, ) stream = nil return Enumerator.new do |events| stream ||= build_stream_run(payload).each stream.each { |event| events << event } end end stream(, **) end |