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 live source-tagged Agent progress, coordination events, and the final result.
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. Every member publishes live progress to the Run, including members that hand off. The final result selects the answering member without replaying its text. Applications filter sources for the destination before forwarding progress; see Run.
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 178 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| @framework_prompt_invocation_paths = template_paths 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) 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