class LittleGhost::Assembly
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
Runreturns its childRunResult. stream_ask(...).each { |event| ... }-
A standalone stream returns its top-level
Runafter enumeration. A run-scoped stream ends with aninvocation_stopevent carryingRunResult.
Attributes
Runtime used to resolve participants and build Runs.
Public Class Methods
Source
# File lib/little_ghost/assembly.rb, line 52 def ask(message, **options) definition.implementation.new.ask(message, **options) end
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.
# File lib/little_ghost/assembly.rb, line 109 def assembly_id(*values) return assembly_id_value || default_assembly_id if values.empty? self.assembly_id_value = values.fetch(0).to_s end
The stable identifier used for tools and telemetry. Named subclasses derive it from their underscored class name without their type suffix.
Source
# File lib/little_ghost/assembly.rb, line 127 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
Returns :agent, :workflow, :swarm, :graph, or :assembly.
Source
# File lib/little_ghost/assembly.rb, line 75 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
Returns an immutable definition for this class.
# File lib/little_ghost/assembly.rb, line 120 def description(*values) return description_value.to_s if values.empty? self.description_value = values.fetch(0).to_s end
The human-readable description used when exposing the assembly as a tool.
# File lib/little_ghost/assembly.rb, line 65 def stream_ask(message, **options) snapshot = definition stream = nil Enumerator.new do |events| stream ||= snapshot.implementation.new.stream_ask(message, **options) stream.each { |event| events << event } end end
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.
Source
# File lib/little_ghost/assembly.rb, line 93 def to_builder builder_class = { agent: AgentBuilder, workflow: WorkflowBuilder, swarm: SwarmBuilder, graph: GraphBuilder }.fetch(assembly_kind) builder_class.new(base: self) end
Returns a mutable dynamic builder seeded by this class.
Public Instance Methods
Source
# File lib/little_ghost/assembly.rb, line 251 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 options = { 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) options[:interjection_metadata] = context&.interjection_metadata options[:interjection_ids] = context&.interjection_ids || [] end result = target.call(input.fetch("input"), **options) 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.messages.reject { |message| message.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
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.
Source
# File lib/little_ghost/assembly.rb, line 217 def ask(message, **options) call(message, **options) end
# File lib/little_ghost/assembly.rb, line 203 def call(input = nil, **options) return build_run(entrypoint_payload(input, options)).call if standalone? result = nil stream(input, **options).each do |event| result = event.data[:result] if event.type == :invocation_stop end result end
Source
# File lib/little_ghost/assembly.rb, line 327 def close @assembly_mutex.synchronize do return if @assembly_closed @assembly_closed = true end end
Closes resources owned directly by this assembly.
# File lib/little_ghost/assembly.rb, line 312 def interject(message, **options) 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(message, **options) end
Adds an interjection to the single active leaf Agent.
Source
# File lib/little_ghost/assembly.rb, line 338 def prompt_locals = {}
Additional prompt locals made available to child agents.
# File lib/little_ghost/assembly.rb, line 194 def start_execution(payload, &event_consumer) ensure_standalone! Execution.start(build_stream_run(payload), &event_consumer) end
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.
# File lib/little_ghost/assembly.rb, line 229 def stream_ask(message, **options) if standalone? options[:deadline_at] = options.delete(:deadline) if options.key?(:deadline) payload = entrypoint_payload(message, options) stream = nil return Enumerator.new do |events| stream ||= build_stream_run(payload).each stream.each { |event| events << event } end end stream(message, **options) end
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.