Skip to content
tnsaiDocs
GitHub

Fan-out and agent pools

Send one message to many agents and combine what comes back with ScatterGather. Reuse agents across requests with LazyAgentPool. Both live in tnsai-coordination.

Scatter-gather

ScatterGather is a static utility. Every method sends the same message to each agent through agent.chat(...) on a thread pool of up to 10 threads, and collects an AgentResponse per agent.

MethodReturnsWaits for
scatter(message, agents, timeout, unit)ScatterResultsEvery agent, or the timeout. scatter(message, agents) uses 60 seconds.
race(message, agents, timeout, unit)Optional<AgentResponse>The first successful response. Failed responses do not win.
quorum(message, agents, quorumSize, timeout, unit)ScatterResultsquorumSize successful responses, every agent, or the timeout
gatherWithScore(message, agents, scorer, timeout, unit)Optional<AgentResponse>Runs scatter, then picks the successful response with the highest scorer value
gatherWithConsensus(message, agents, timeout, unit)Optional<AgentResponse>Runs scatter, then picks the response that the most other responses resemble
gatherAndAggregate(message, agents, aggregator, timeout, unit)Optional<String>Runs scatter, then asks the aggregator agent to merge the successful answers
package com.example.tnsai.docs;

import com.tnsai.agents.Agent;
import com.tnsai.coordination.ScatterGather;
import com.tnsai.coordination.ScatterGather.AgentResponse;
import com.tnsai.coordination.ScatterGather.ScatterResults;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/** Compilation contract for multi-agent/fan-out.md scatter-gather. */
public final class FanOutExample {

    private FanOutExample() {
    }

    static void review(String question, List<Agent> experts, Agent editor) {
        // Ask everyone and wait up to 30 seconds.
        ScatterResults all = ScatterGather.scatter(question, experts, 30, TimeUnit.SECONDS);
        System.out.println("Answered: " + all.successful().size() + "/" + all.totalAgents());
        all.fastest().ifPresent(r -> System.out.println("Fastest: " + r.agentName()));

        // Take the first successful answer.
        Optional<AgentResponse> first = ScatterGather.race(question, experts, 10, TimeUnit.SECONDS);
        first.ifPresent(r -> System.out.println(r.agentName() + ": " + r.response()));

        // Stop waiting once two agents have answered.
        ScatterResults two = ScatterGather.quorum(question, experts, 2, 30, TimeUnit.SECONDS);
        System.out.println("Quorum answers: " + two.responseTexts());

        // Let an editor agent merge the answers.
        Optional<String> merged = ScatterGather.gatherAndAggregate(
            question, experts, editor, 30, TimeUnit.SECONDS);
        merged.ifPresent(System.out::println);
    }
}

Results

AgentResponse is a record with agentId, agentName, response, durationMs, success and error. An agent that throws becomes a failed response with the exception message as error; an agent still running at the timeout becomes a failed response with the error Timeout.

ScatterResults holds responses, totalDurationMs and totalAgents, plus these helpers:

HelperReturns
successful() / failed()The responses split by success
responseTexts()The text of each successful response
fastest()The successful response with the lowest durationMs
successRate()Successful responses divided by totalAgents

quorum returns only the responses collected so far, so responses can hold fewer entries than totalAgents. The agents still running are interrupted.

Consensus and aggregation

gatherWithConsensus compares responses word by word. Two responses count as similar when the Jaccard similarity of their lower-cased word sets is above 0.5. The response similar to the most others wins. It does not compare meaning, so use gatherWithScore or an aggregator when answers are phrased differently.

gatherAndAggregate returns the single answer as-is when only one agent succeeds. Otherwise it sends the aggregator the original question and each successful answer, labelled with the agent's name, and returns the aggregator's reply. If the aggregator fails, it returns the first successful answer.

Agent pools

LazyAgentPool creates agents through an AgentFactory when they are checked out and keeps returned agents idle for reuse. checkout() returns an AgentHandle. Use it in try-with-resources: close() returns the agent to the pool.

package com.example.tnsai.docs;

import com.tnsai.agents.Agent;
import com.tnsai.coordination.pool.ActivationPolicy;
import com.tnsai.coordination.pool.AgentFactory;
import com.tnsai.coordination.pool.AgentHandle;
import com.tnsai.coordination.pool.LazyAgentPool;
import com.tnsai.coordination.pool.PoolMetrics;
import com.tnsai.coordination.pool.PredictiveActivator;

import java.util.concurrent.TimeUnit;

/** Compilation contract for multi-agent/fan-out.md agent pool. */
public final class AgentPoolExample {

    private AgentPoolExample() {
    }

    static void serve(AgentFactory factory, String task) {
        LazyAgentPool pool = LazyAgentPool.builder()
            .factory(factory)
            .minIdle(2)
            .maxSize(20)
            .idleTimeout(5, TimeUnit.MINUTES)
            .policy(ActivationPolicy.PREWARMED)
            .build();

        PredictiveActivator activator = PredictiveActivator.builder()
            .pool(pool)
            .windowSize(60)        // seconds
            .burstThreshold(5)     // checkouts per window
            .preWarmCount(3)
            .analyzeInterval(10)   // seconds
            .build();
        activator.start();

        try (AgentHandle handle = pool.checkout()) {
            String answer = handle.execute(task);
            Agent agent = handle.getAgent();
            System.out.println(agent.name() + ": " + answer);
        }   // close() returns the agent to the pool

        PoolMetrics metrics = pool.getMetrics();
        System.out.println("Active " + metrics.active() + ", idle " + metrics.idle()
            + ", peak " + metrics.peakActive());

        activator.stop();
        pool.close();
    }
}
Builder methodDefaultMeaning
factory(AgentFactory)RequiredCreates agents. AgentFactory is a functional interface with Agent create().
minIdle(int)0Idle agents the evictor keeps
maxSize(int)10Upper limit on active plus idle agents. Must be at least minIdle.
idleTimeout(long, TimeUnit)60 secondsSets the eviction interval
policy(ActivationPolicy)ON_DEMANDWhen agents are created
listener(PoolListener)NoneReceives pool events

checkout() throws IllegalStateException when the pool is closed, or when maxSize agents already exist. In the second case listeners get onExhausted() first. AgentHandle also has getAgent() and getCheckoutDurationMs().

Activation and eviction

ActivationPolicyBehavior
ON_DEMANDCreates an agent at checkout when none is idle
PREWARMEDAlso creates minIdle agents when the pool is built
BURSTDeclared, but the pool treats it like ON_DEMAND

Every half of idleTimeout (at least once per second), the pool destroys idle agents above minIdle. It does not check how long each agent has been idle, so agents added by preWarm are removed at the next run when they exceed minIdle. A returned agent is destroyed instead of kept when the idle queue already holds maxSize agents.

preWarm(count) creates up to count idle agents without exceeding maxSize and returns how many it created. close() stops the evictor and destroys the agents.

Metrics and listeners

getMetrics() returns a PoolMetrics record with active, idle, totalCreated, totalDestroyed and peakActive. addListener and removeListener manage PoolListener instances. Its methods onActivated, onReturned, onDestroyed and onExhausted all default to no-ops.

Predictive pre-warming

PredictiveActivator registers itself as a listener on the pool. After start(), it checks the last windowSize seconds of checkouts every analyzeInterval seconds. When the window holds at least burstThreshold checkouts, or any exhaustion, it tops the idle count up to preWarmCount with preWarm. The defaults are a 60-second window, a threshold of 5, 3 agents and a 10-second interval. stop() ends the checks and removes the listener. recentActivationCount() and recentExhaustionCount() report the current window.

Because the evictor removes idle agents above minIdle, pre-warmed agents last only until the next eviction unless minIdle covers them.