class LittleGhost::Swarm
Lets configured Agent members hand one request directly to one another.
A swarm is an Assembly for model-selected routing. One member is active at a time. It either produces the final answer or uses a model-visible handoff tool to choose one of the next members allowed by the application.
class ProblemSolverSwarm < LittleGhost::Swarm member TriageAgent member BillingAgent member AccountAgent start TriageAgent handoff TriageAgent, to: [BillingAgent, AccountAgent] max_steps 12 end run = ProblemSolverSwarm.ask("Why was I charged twice?") run.response
Call a named Swarm with ask for its final Run, or the streaming entrypoint for coordination and final-response events.
Swarm members are Agent definitions rather than arbitrary assemblies so a handoff remains a direct model-to-model transition. Original conversation history and application context stay isolated unless a member opts in with history: true or context: true. Streams expose coordination events and the final member response, but not intermediate model text.
Public Class Methods
Source
# File lib/little_ghost/swarm.rb, line 78 def handoff(from, to:) from = normalize_member_id(member_reference_id(from)) targets = Array(to).map { |target| normalize_member_id(member_reference_id(target)) } raise ArgumentError, "handoff requires at least one target" if targets.empty? raise ArgumentError, "handoff targets must be unique" unless targets.uniq.length == targets.length declaration = Handoff.new(from:, to: targets.freeze) self.swarm_handoffs_value = (swarm_handoffs_value + [declaration]).freeze declaration end
Restricts one member to the declared handoff targets.
# File lib/little_ghost/swarm.rb, line 103 def max_handoff_repeats(value = nil) return swarm_max_handoff_repeats_value if value.nil? value = Integer(value) raise ArgumentError, "max_handoff_repeats must be at least 1" if value < 1 self.swarm_max_handoff_repeats_value = value end
Source
# File lib/little_ghost/swarm.rb, line 90 def max_steps(value = nil) return swarm_max_steps_value if value.nil? value = Integer(value) raise ArgumentError, "max_steps must be at least 1" if value < 1 self.swarm_max_steps_value = value end
Reads or assigns the maximum member executions.
# File lib/little_ghost/swarm.rb, line 52 def member(agent, as: nil, timeout: nil, retries: 0, retry_on: nil, retry_delay: 0, history: false, context: false) validate_agent_reference!(agent) id = normalize_member_id(as || agent_reference_id(agent)) raise ConfigurationError, "swarm member #{id.inspect} is already declared" if swarm_members_value.key?(id) unless [history, context].all? { |value| value == true || value == false } raise ArgumentError, "swarm member history and context options must be true or false" end policies = {timeout:, retries:, retry_on:, retry_delay:}.freeze declaration = Member.new( id:, agent:, policies:, inherit_history: history, inherit_context: context ) self.swarm_members_value = swarm_members_value.merge(id => declaration).freeze end
Declares one Agent member and its optional execution policy.
Source
# File lib/little_ghost/swarm.rb, line 71 def start(member = nil) return swarm_start_value if member.nil? self.swarm_start_value = normalize_member_id(member_reference_id(member)) end
Reads or assigns the initial Agent member.
Source
# File lib/little_ghost/swarm.rb, line 113 def validate! swarm_definition! self end
Validates the members and allowed handoff routes, then returns this class.
Public Instance Methods
# File lib/little_ghost/swarm.rb, line 177 def stream(input = nil, history: nil, context: nil, cancellation_token: Support::CancellationToken.new, deadline: nil, settings: nil, template_locals: nil, template_paths: nil, parent_operation_id: nil, checkpoint: nil, **_options) raise ArgumentError, "input is required" if input.nil? if standalone? return build_run(entrypoint_payload(input, { history:, context:, settings:, template_paths:, deadline_at: deadline, cancellation_token: }.compact)).each end reserve_execution! current_input = input.is_a?(Message) ? input : Message.new(role: :user, content: input) original_history = normalize_history(history) original_context = isolated_assembly_state(context || {}) settings ||= {} template_locals ||= {} template_paths ||= [] usage = Usage.new error_emitted = false Enumerator.new do |events| members, current, topology = self.class.swarm_definition! steps = [] transitions = Hash.new(0) step_number = 0 previous_step_id = nil loop do check_swarm_control!(cancellation_token, deadline, step_number) step_number += 1 member = members.fetch(current) allowed = allowed_targets(current, members, topology) handoff_tool = handoff_tool_for(current, allowed, members) if allowed.any? step_id = SecureRandom.uuid events << StreamEvent.build( :assembly_step_start, assembly_id: self.class.assembly_id, assembly_kind: :swarm, step: step_number, participant: current, step_id: ) execution = execute_assembly_step( reference: member.agent, participant: current, input: current_input, history: member.inherit_history ? original_history : [], context: member.inherit_context ? original_context : {}, cancellation_token:, deadline:, settings:, template_locals:, template_paths:, parent_operation_id:, policies: member.policies, predecessor_ids: Array(previous_step_id), checkpoint: nil, build_options: {tools: [handoff_tool].compact}, step_id: ) { |event| events << event } result = execution.result previous_step_id = execution.step.id transition = active_transition(execution, allowed) events << StreamEvent.build( :assembly_step_stop, assembly_id: self.class.assembly_id, assembly_kind: :swarm, step: step_number, participant: current, step_id: execution.step.id, usage: execution.step.usage ) if transition usage += execution.step.usage target = transition.fetch(:agent_id) key = [current, target] transitions[key] += 1 if transitions[key] > self.class.max_handoff_repeats events << StreamEvent.build( :assembly_handoff_loop, assembly_id: self.class.assembly_id, assembly_kind: :swarm, from: current, to: target, count: transitions[key] ) raise AssemblyLimitError, "swarm repeated handoff #{current.inspect} -> #{target.inspect} too many times" end sanitized = sanitized_handoff_step(execution.step, transition) steps << sanitized steps.concat(result.steps.drop(1)) events << StreamEvent.build( :assembly_transition, assembly_id: self.class.assembly_id, assembly_kind: :swarm, step: step_number, from: current, to: target ) current_input = handoff_input(from: current, transition:) current = target next end usage += execution.step.usage steps.concat(result.steps) final = copy_run_result(result, usage:, steps: steps.freeze) release_final_events(execution.events, final).each { |event| events << event } break end rescue => error usage += step_error_usage(error) unless error_emitted events << StreamEvent.build(:invocation_error, error:, usage:, metadata: {}) end raise end end
Streams lifecycle events and only the final member’s answer events.